# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 Description: "(SO0062) - Distributed Load Testing on AWS is a reference architecture to perform application load testing at scale. Version v1.2.0" Parameters: AdminName: Type: String Description: Admin user name to access the Distributed Load Testing Console MinLength: 4 MaxLength: 20 AllowedPattern: '[a-zA-Z0-9-]+' ConstraintDescription: "Admin username must be a minimum of 4 characters and cannot include spaces" AdminEmail: Type: String Description: Admin user email address to access the Distributed Load Testing Console MinLength: 5 AllowedPattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' ConstraintDescription: "Admin email must be a valid email address" VpcCidrBlock: Type: String Default: 192.168.0.0/16 Description: CIDR block of the new VPC where AWS Fargate will be placed AllowedPattern: "(?:^$|(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2}))" ConstraintDescription: "must be a valid IP CIDR range of the form x.x.x.x/x." MinLength: 9 MaxLength: 18 SubnetACidrBlock: Type: String Default: 192.168.0.0/20 Description: CIDR block for subnet A of the AWS Fargate VPC AllowedPattern: "(?:^$|(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2}))" ConstraintDescription: "must be a valid IP CIDR range of the form x.x.x.x/x." MinLength: 9 MaxLength: 18 SubnetBCidrBlock: Type: String Default: 192.168.16.0/20 Description: CIDR block for subnet B of the AWS Fargate VPC AllowedPattern: "(?:^$|(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2}))" ConstraintDescription: "must be a valid IP CIDR range of the form x.x.x.x/x." EgressCidr: Type: String Default: 0.0.0.0/0 Description: CIDR Block to restrict the ECS container outbound access MinLength: 9 MaxLength: 18 AllowedPattern: "(?:^$|(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2}))" ConstraintDescription: "must be a valid IP CIDR range of the form x.x.x.x/x." Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Console Access" Parameters: - AdminName - AdminEmail - Label: default: "AWS Fargate VPC Settings" Parameters: - VpcCidrBlock - SubnetACidrBlock - SubnetBCidrBlock - EgressCidr ParameterLabels: AdminName: default: "Console Administrator Name" AdminEmail: default: "Console Administrator Email" VpcCidrBlock: default: "AWS Fargate VPC CIDR Block" SubnetACidrBlock: default: "AWS Fargate Subnet A CIDR Block" SubnetBCidrBlock: default: "AWS Fargate Subnet B CIDR Block" EgressCidr: default: "AWS Fargate SecurityGroup CIDR Block" Mappings: SourceCode: General: S3Bucket: solutions KeyPrefix: distributed-load-testing-on-aws/v1.2.0 AnonymousData: SendAnonymousData: Data: "Yes" URL: https://metrics.awssolutionsbuilder.com/generic Conditions: Metrics: !Equals [ !FindInMap [AnonymousData, SendAnonymousData, Data], "Yes" ] Resources: ## Roles and Permissions CloudWatchLogsPolicy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub ${AWS::StackName}-cloudwatch-policy Roles: - !Ref LambdaApiRole - !Ref LambdaResultsRole - !Ref EcsTaskExecutionRole # - !Ref ContainerCodeBuildRole # - !Ref ContainerCodePipelineRole # - !Ref LambdaEcrCheckerRole - !Ref LambdaTaskStatusRole - !Ref LambdaCustomRole - !Ref LambdaTaskRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/* S3Policy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub ${AWS::StackName}-s3-policy Roles: - !Ref LambdaApiRole - !Ref LambdaResultsRole - !Ref EcsTaskExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:HeadObject - s3:PutObject - s3:GetObject - s3:ListBucket Resource: - !Sub ${ScenariosBucket.Arn} - !Sub ${ScenariosBucket.Arn}/* DynamoDbPolicy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub ${AWS::StackName}-dynamodb-policy Roles: - !Ref LambdaApiRole - !Ref LambdaResultsRole - !Ref LambdaTaskRole # - !Ref LambdaEcrCheckerRole - !Ref LambdaTaskStatusRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:DeleteItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:Scan - dynamodb:UpdateItem Resource: - !Sub ${ScenariosTable.Arn} LambdaApiRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${AWS::StackName}-api-lambda-policy PolicyDocument: Statement: - Effect: Allow Action: - ecs:ListTasks Resource: - "*" - Effect: Allow Action: - ecs:RunTask - ecs:StopTask - ecs:DescribeTasks Resource: - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task/* - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/*:* - Effect: Allow Action: - iam:PassRole Resource: - !Sub ${EcsTaskExecutionRole.Arn} - Effect: Allow Action: - states:StartExecution Resource: - !Ref TaskRunnerStepFunctions Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: ecs:listTasks does not support resource level permissions. LambdaTaskRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${AWS::StackName}-task-lambda-policy PolicyDocument: Statement: - Effect: Allow Action: - ecs:RunTask Resource: - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task/* - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/*:* - Effect: Allow Action: - iam:PassRole Resource: - !Sub ${EcsTaskExecutionRole.Arn} LambdaTaskStatusRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${AWS::StackName}-task-status-lambda-policy PolicyDocument: Statement: - Effect: Allow Action: - ecs:ListTasks Resource: - "*" - Effect: Allow Action: - ecs:DescribeTasks - ecs:StopTask Resource: - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task/* Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: ecs:ListTasks cannot specify resource level. # LambdaEcrCheckerRole: # Type: AWS::IAM::Role # Properties: # AssumeRolePolicyDocument: # Version: 2012-10-17 # Statement: # - Effect: Allow # Principal: # Service: # - lambda.amazonaws.com # Action: # - sts:AssumeRole # Policies: # - PolicyName: !Sub ${AWS::StackName}-ecr-checker-policy # PolicyDocument: # Statement: # - Effect: Allow # Action: # - ecr:DescribeImages # Resource: # - !GetAtt EcrRepository.Arn # - Effect: Allow # Action: # - codebuild:ListBuildsForProject # - codebuild:BatchGetBuilds # Resource: # - !GetAtt ContainerCodeBuild.Arn LambdaResultsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${AWS::StackName}-api-lambda-policy PolicyDocument: Statement: - Effect: Allow Action: - cloudwatch:GetMetricWidgetImage Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: cloudwatch:GetMetricWidgetImage does not support resource level permisions. LambdaCustomRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${AWS::StackName}-custom-resource-policy PolicyDocument: Statement: - Effect: Allow Action: - s3:PutObject Resource: - !Sub ${ContainerBucket.Arn}/* - !Sub ${ConsoleBucket.Arn}/* - Effect: Allow Action: - s3:GetObject Resource: - !Sub - arn:${AWS::Partition}:s3:::${Bucket}-${AWS::Region}/* - Bucket: !FindInMap ["SourceCode", "General", "S3Bucket"] EcsTaskExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "sts:AssumeRole" Principal: Service: "ecs-tasks.amazonaws.com" ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" # ContainerCodeBuildRole: # Type: AWS::IAM::Role # Properties: # AssumeRolePolicyDocument: # Version: "2012-10-17" # Statement: # - Effect: Allow # Action: sts:AssumeRole # Principal: # Service: codebuild.amazonaws.com # Policies: # - PolicyName: !Sub ${AWS::StackName}-code-build-policy # PolicyDocument: # Statement: # - Effect: Allow # Action: # - ecr:DescribeImages # - ecr:PutImage # - ecr:UploadLayerPart # - ecr:CompleteLayerUpload # - ecr:InitiateLayerUpload # - ecr:GetDownloadUrlForLayer # - ecr:ListImages # - ecr:BatchCheckLayerAvailability # - ecr:GetRepositoryPolicy # Resource: # - !Sub ${EcrRepository.Arn} # - Effect: Allow # Action: # - ecr:GetAuthorizationToken # Resource: "*" # - Effect: Allow # Action: # - s3:GetObject # - s3:GetObjectVersion # - s3:GetBucketVersioning # - s3:GetObjectVersion # - s3:ListBucketVersions # - s3:PutObject # Resource: # - !Sub ${ContainerBucket.Arn} # - !Sub ${ContainerBucket.Arn}/* # Metadata: # cfn_nag: # rules_to_suppress: # - id: W11 # reason: ecr:GetAuthorizationToken does not support resource level permission. # ContainerCodePipelineRole: # Type: AWS::IAM::Role # Properties: # AssumeRolePolicyDocument: # Version: 2012-10-17 # Statement: # - Effect: Allow # Principal: # Service: # - codepipeline.amazonaws.com # Action: # - sts:AssumeRole # Policies: # - PolicyName: !Sub ${AWS::StackName}-code-pipeline-policy # PolicyDocument: # Statement: # - Effect: Allow # Action: # - s3:GetObject # - s3:GetObjectVersion # - s3:GetBucketVersioning # - s3:ListBucketVersions # - s3:PutObject # Resource: # - !Sub ${ContainerBucket.Arn} # - !Sub ${ContainerBucket.Arn}/* # - Effect: Allow # Action: # - codebuild:StartBuild # - codebuild:BatchGetBuilds # Resource: # - !Sub ${ContainerCodeBuild.Arn} ApiLambdaInvoke: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt ApiServices.Arn Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${Api}/* ## Fargate VPC Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidrBlock InstanceTenancy: default EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W60 reason: This VPC is used by Fargates only, so it does not require to have VPC flow logs. PublicSubnetA: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref SubnetACidrBlock AvailabilityZone: !Select - 0 - !GetAZs VpcId: !Ref Vpc PublicSubnetB: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref SubnetBCidrBlock AvailabilityZone: !Select - 0 - !GetAZs VpcId: !Ref Vpc InternetGateway: Type: AWS::EC2::InternetGateway Properties: {} MainRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc GatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref Vpc InternetGatewayId: !Ref InternetGateway RouteToInternet: Type: AWS::EC2::Route DependsOn: GatewayAttachment Properties: DestinationCidrBlock: 0.0.0.0/0 RouteTableId: !Ref MainRouteTable GatewayId: !Ref InternetGateway RouteTableAssociationA: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref MainRouteTable SubnetId: !Ref PublicSubnetA RouteTableAssociationB: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref MainRouteTable SubnetId: !Ref PublicSubnetB ## ECS Resources EcrRepository: # stack delete will fail as the repo is not empty. DeletionPolicy: Retain Type: AWS::ECR::Repository EcsCluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Sub ${AWS::StackName} ClusterSettings: - Name: containerInsights Value: enabled Tags: - Key: SolutionId Value: SO0062 EcsSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: DLTS Tasks Security Group VpcId: !Ref Vpc SecurityGroupEgress: - IpProtocol: '-1' CidrIp: !Ref EgressCidr Metadata: cfn_nag: rules_to_suppress: - id: W36 reason: "flagged as not having a Description, property is GroupDescription not Description" - id: W40 reason: "IpProtocol set to -1 (any) as ports are not known prior to running tests" EcsTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Cpu: '512' ExecutionRoleArn: !GetAtt EcsTaskExecutionRole.Arn Memory: '2048' NetworkMode: awsvpc RequiresCompatibilities: - FARGATE TaskRoleArn: !GetAtt EcsTaskExecutionRole.Arn ContainerDefinitions: - Essential: true Name: !Sub ${AWS::StackName}-load-tester # Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}:latest Image: public.ecr.aws/p9r6s5p7/dlt:latest Memory: 2048 LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref EcsCloudWatchLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: "load-testing" EcsCloudWatchLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 365 EcsAvgResponseTime: Type: AWS::Logs::MetricFilter Properties: FilterPattern: "[time, logType=INFO*, logTitle=Current*, numVu, vu, numSucc, succ, numFail, fail, avgRt, x]" LogGroupName: !Ref EcsCloudWatchLogGroup MetricTransformations: - MetricValue: "$avgRt" MetricNamespace: "distribuited-load-testing" MetricName: "Avg Response Time" EcsLoadTesting: Type: AWS::CloudWatch::Dashboard Properties: DashboardBody: !Sub '{"widgets":[{"type":"metric","x":0,"y":0,"width":8,"height":8,"properties":{"metrics":[["distribuited-load-testing","Avg Response Time"]],"view":"timeSeries","stacked":true,"region":"${AWS::Region}","stat":"Average"}}]}' ## Storage ScenariosBucket: DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: LoggingConfiguration: DestinationBucketName: !Ref LogsBucket LogFilePrefix: scenarios-bucket-access/ BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true CorsConfiguration: CorsRules: - AllowedMethods: - GET - POST - PUT AllowedOrigins: - !Sub https://${ConsoleCloudFront.DomainName} AllowedHeaders: - '*' Tags: - Key: SolutionId Value: SO0062 ScenariosBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref ScenariosBucket PolicyDocument: Statement: - Action: "s3:*" Effect: Deny Principal: "*" Resource: - !Sub ${ScenariosBucket.Arn} - !Sub ${ScenariosBucket.Arn}/* Condition: Bool: aws:SecureTransport: false ContainerBucket: DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: VersioningConfiguration: Status: Enabled LoggingConfiguration: DestinationBucketName: !Ref LogsBucket LogFilePrefix: container-bucket-access/ BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W51 reason: Since the bucket does not allow the public access, it does not require to have bucket policy. LogsBucket: DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: AccessControl: LogDeliveryWrite BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: Logging not enabled, this is the logs bucket for CloudFront and the other S3 buckets. - id: W51 reason: Since the bucket does not allow the public access, it does not require to have bucket policy. ConsoleBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: WebsiteConfiguration: IndexDocument: index.html ErrorDocument: index.html LoggingConfiguration: DestinationBucketName: !Ref LogsBucket LogFilePrefix: console-bucket-access/ PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 Tags: - Key: SolutionId Value: SO0062 ScenariosTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: testId AttributeType: S KeySchema: - AttributeName: testId KeyType: HASH BillingMode: PAY_PER_REQUEST SSESpecification: SSEEnabled: True Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W78 reason: The table does not require PITR. ## CloudFront ConsoleBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref ConsoleBucket PolicyDocument: Statement: - Effect: Allow Principal: CanonicalUser: !GetAtt ConsoleOriginAccessIdentity.S3CanonicalUserId Action: s3:GetObject Resource: !Sub ${ConsoleBucket.Arn}/* ConsoleOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: !Sub "access-identity-${ConsoleBucket}" ConsoleCloudFront: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Comment: "Website distribution for solution" Logging: IncludeCookies: false Bucket: !GetAtt LogsBucket.DomainName Prefix: cloudfront-logs/ Origins: - Id: console DomainName: !Sub "${ConsoleBucket}.s3.${AWS::Region}.amazonaws.com" OriginPath: /console S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${ConsoleOriginAccessIdentity}" DefaultCacheBehavior: TargetOriginId: console AllowedMethods: - GET - HEAD - OPTIONS - PUT - POST - PATCH - DELETE CachedMethods: - GET - HEAD - OPTIONS ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https IPV6Enabled: true DefaultRootObject: "index.html" CustomErrorResponses: - ErrorCode: 404 ResponsePagePath: "/index.html" ResponseCode: 200 - ErrorCode: 403 ResponsePagePath: "/index.html" ResponseCode: 200 ViewerCertificate: CloudFrontDefaultCertificate: true Enabled: true HttpVersion: 'http2' Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W70 reason: With the default CF certificate, we cannot set up the minimum protocol to TLS 1.2. ## Container Pipeline # ContainerCodeBuild: # Type: AWS::CodeBuild::Project # Properties: # Description: Builds distributed load testing suite # TimeoutInMinutes: 20 # ServiceRole: !GetAtt ContainerCodeBuildRole.Arn # EncryptionKey: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3 # Artifacts: # Type: CODEPIPELINE # Environment: # Type: LINUX_CONTAINER # ComputeType: BUILD_GENERAL1_MEDIUM # Image: aws/codebuild/standard:4.0 # PrivilegedMode: True # EnvironmentVariables: # - Name: REPOSITORY # Value: !Sub ${EcrRepository} # - Name: REPOSITORY_URI # Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository} # Source: # Type: CODEPIPELINE # BuildSpec: !Sub | # version: 0.2 # phases: # pre_build: # commands: # - aws --version # - echo $REPOSITORY # - echo $REPOSITORY_URI # - aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin $REPOSITORY_URI # build: # commands: # - docker build -t $REPOSITORY:latest . # - docker tag $REPOSITORY:latest $REPOSITORY_URI:latest # post_build: # commands: # - docker push $REPOSITORY_URI:latest # Tags: # - Key: SolutionId # Value: SO0062 # ContainerCodePipeline: # #wait for the container # DependsOn: [ CopyDockerFile, CloudWatchLogsPolicy] # Type: AWS::CodePipeline::Pipeline # Properties: # RoleArn: !GetAtt ContainerCodePipelineRole.Arn # ArtifactStore: # Type: S3 # Location: !Ref ContainerBucket # Stages: # - Name: Source # Actions: # - Name: Source # ActionTypeId: # Category: Source # Provider: S3 # Owner: AWS # Version: '1' # OutputArtifacts: # - Name: SourceOutput # Configuration: # S3Bucket: !Ref ContainerBucket # S3ObjectKey: container.zip # - Name: Build # Actions: # - Name: Build # ActionTypeId: # Category: Build # Owner: AWS # Version: '1' # Provider: CodeBuild # InputArtifacts: # - Name: SourceOutput # OutputArtifacts: # - Name: BuildOutput # Configuration: # ProjectName: # !Ref ContainerCodeBuild # Tags: # - Key: SolutionId # Value: SO0062 ## Lambda CustomResource: DependsOn: ScenariosBucket Type: AWS::Lambda::Function Properties: Description: CFN Lambda backed custom resource to deploy assets to s3 Handler: index.handler Role: !GetAtt LambdaCustomRole.Arn Code: # https://solutions-us-east-1.s3.amazonaws.com/distributed-load-testing-on-aws/v1.2.0/custom-resource.zip S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "custom-resource.zip"]] Runtime: nodejs12.x Timeout: 120 Environment: Variables: METRIC_URL: !FindInMap [AnonymousData, SendAnonymousData, URL] Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. ApiServices: Type: AWS::Lambda::Function Properties: Description: api microservices for creating, updating, listing and deleting test scenarios Handler: index.handler Role: !GetAtt LambdaApiRole.Arn Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "api-services.zip"]] Runtime: nodejs12.x Timeout: 120 Environment: Variables: SCENARIOS_BUCKET: !Ref ScenariosBucket SCENARIOS_TABLE: !Ref ScenariosTable TASK_CLUSTER: !Ref EcsCluster STATE_MACHINE_ARN: !Ref TaskRunnerStepFunctions SOLUTION_ID: SO0062 UUID: !GetAtt Uuid.UUID VERSION: v1.2.0 SEND_METRIC: !FindInMap [AnonymousData, SendAnonymousData, Data] METRIC_URL: !FindInMap [AnonymousData, SendAnonymousData, URL] Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. ResultsParser: Type: AWS::Lambda::Function Properties: Description: result parser for indexing xml test results to DynamoDB Handler: index.handler Role: !GetAtt LambdaResultsRole.Arn Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "results-parser.zip"]] Runtime: nodejs12.x Timeout: 120 Environment: Variables: SCENARIOS_BUCKET: !Ref ScenariosBucket SCENARIOS_TABLE: !Ref ScenariosTable SOLUTION_ID: SO0062 UUID: !GetAtt Uuid.UUID VERSION: v1.2.0 SEND_METRIC: !FindInMap [AnonymousData, SendAnonymousData, Data] METRIC_URL: !FindInMap [AnonymousData, SendAnonymousData, URL] Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. TaskRunner: Type: AWS::Lambda::Function Properties: Description: Task runner for ECS task definitions Handler: index.handler Role: !GetAtt LambdaTaskRole.Arn Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "task-runner.zip"]] Runtime: nodejs12.x Timeout: 180 Environment: Variables: SCENARIOS_BUCKET: !Ref ScenariosBucket SCENARIOS_TABLE: !Ref ScenariosTable TASK_CLUSTER: !Ref EcsCluster TASK_DEFINITION: !Ref EcsTaskDefinition TASK_SECURITY_GROUP: !Ref EcsSecurityGroup TASK_IMAGE: !Sub ${AWS::StackName}-load-tester SUBNET_A: !Ref PublicSubnetA SUBNET_B: !Ref PublicSubnetB API_INTERVAL: 10 Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. TaskStatusChecker: Type: AWS::Lambda::Function Properties: Description: Task status checker Handler: index.handler Role: !GetAtt LambdaTaskStatusRole.Arn Code: # https://solutions-us-east-1.s3.amazonaws.com/distributed-load-testing-on-aws/v1.2.0/task-status-checker.zip S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "task-status-checker.zip"]] Runtime: nodejs12.x Timeout: 180 Environment: Variables: TASK_CLUSTER: !Ref EcsCluster SCENARIOS_TABLE: !Ref ScenariosTable Tags: - Key: SolutionId Value: SO0062 Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. # EcrChecker: # Type: AWS::Lambda::Function # Properties: # Description: ECR readiness checker # Handler: index.handler # Role: !GetAtt LambdaEcrCheckerRole.Arn # Code: # S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] # S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "ecr-checker.zip"]] # Runtime: nodejs12.x # Timeout: 60 # Environment: # Variables: # CODE_BUILD_PROJECT: !Ref ContainerCodeBuild # ECR_REPOSITORY_NAME: !Ref EcrRepository # SCENARIOS_TABLE: !Ref ScenariosTable # Tags: # - Key: SolutionId # Value: SO0062 # Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CloudWatchLogsPolicy covers a permission to write CloudWatch logs. ## API ApiLoggingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: apigateway.amazonaws.com Version: 2012-10-17 Policies: - PolicyDocument: Statement: - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:DescribeLogGroups - logs:DescribeLogStreams - logs:PutLogEvents - logs:GetLogEvents - logs:FilterLogEvents Effect: Allow Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:* Version: 2012-10-17 PolicyName: ApiCloudWatchRolePolicy Tags: - Key: SolutionId Value: SO0062 ApiAccountConfig: Type: AWS::ApiGateway::Account DependsOn: Api Properties: CloudWatchRoleArn: !GetAtt ApiLoggingRole.Arn ApiLogs: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Retain DeletionPolicy: Retain ApiDeployment: Type: AWS::ApiGateway::Deployment DependsOn: ApiAccountConfig Properties: RestApiId: !Ref Api Description: Production StageName: prod StageDescription: AccessLogSetting: DestinationArn: !GetAtt ApiLogs.Arn Format: "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" Metadata: cfn_nag: rules_to_suppress: - id: W68 reason: Usage plan is not needed for the solution. Api: Type: AWS::ApiGateway::RestApi Properties: Body: swagger: "2.0" info: title: !Sub ${AWS::StackName} basePath: "/prod" schemes: - "https" produces: - application/json x-amazon-apigateway-auth: type: NONE # x-amazon-apigateway-request-validators: # all: # validateRequestBody: true # validateRequestParameters: true # params-only: # validateRequestBody: false # validateRequestParameters: true # x-amazon-apigateway-request-validator: all paths: /scenarios: options: consumes: - "application/json" produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: "when_no_match" requestTemplates: application/json: "{\"statusCode\": 200}" type: "mock" x-amazon-apigateway-any-method: # x-amazon-apigateway-request-validator : all x-amazon-apigateway-auth: type: NONE produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" # security: # - sigv4: [] x-amazon-apigateway-integration: uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServices.Arn}/invocations" responses: default: statusCode: "200" passthroughBehavior: "when_no_match" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws_proxy" /scenarios/{testId}: options: consumes: - "application/json" produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: "when_no_match" requestTemplates: application/json: "{\"statusCode\": 200}" type: "mock" x-amazon-apigateway-any-method: # x-amazon-apigateway-request-validator : params-only x-amazon-apigateway-auth: type: AWS_IAM produces: - "application/json" parameters: - name: "testId" in: "path" required: true type: "string" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" # security: # - sigv4: [] x-amazon-apigateway-integration: uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServices.Arn}/invocations" responses: default: statusCode: "200" passthroughBehavior: "when_no_match" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws_proxy" /tasks: options: consumes: - "application/json" produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: "when_no_match" requestTemplates: application/json: "{\"statusCode\": 200}" type: "mock" x-amazon-apigateway-any-method: # x-amazon-apigateway-request-validator : params-only x-amazon-apigateway-auth: type: NONE produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" # security: # - sigv4: [] x-amazon-apigateway-integration: uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServices.Arn}/invocations" responses: default: statusCode: "200" passthroughBehavior: "when_no_match" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws_proxy" securityDefinitions: sigv4: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" definitions: Empty: type: "object" title: "Empty Schema" ## Cognito # CognitoAuthorizedRole: # Type: AWS::IAM::Role # Properties: # AssumeRolePolicyDocument: # Version: 2012-10-17 # Statement: # - Effect: Allow # Principal: # Federated: cognito-identity.amazonaws.com # Action: # - sts:AssumeRoleWithWebIdentity # Policies: # - PolicyName: !Sub ${AWS::StackName}-congnito-access-role # PolicyDocument: # Version: 2012-10-17 # Statement: # - Effect: Allow # Action: # - execute-api:Invoke # Resource: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${Api}/prod/* # - Effect: Allow # Action: # - s3:PutObject # - s3:GetObject # Resource: !Sub ${ScenariosBucket.Arn}/public/* # CognitoUnAuthorizedRole: # Type: AWS::IAM::Role # Properties: # AssumeRolePolicyDocument: # Version: 2012-10-17 # Statement: # - Effect: Allow # Principal: # Federated: cognito-identity.amazonaws.com # Action: # - sts:AssumeRoleWithWebIdentity # CognitoUserPool: # Type: AWS::Cognito::UserPool # Properties: # UserPoolName: !Sub ${AWS::StackName} # AdminCreateUserConfig: # AllowAdminCreateUserOnly: True # InviteMessageTemplate: # EmailMessage: # !Sub | #
You are invited to join the Distribution Load Testing Solution.
#Username: {username}
#Password: {####}
#Console: https://${ConsoleCloudFront.DomainName}/
# EmailSubject: Welcome to Distributed Load Testing # SMSMessage: "Your username is {username} and temporary password is {####}." # UnusedAccountValidityDays: 7 # AliasAttributes: # - "email" # AutoVerifiedAttributes: # - "email" # EmailVerificationMessage: "Your Distribution Load Testing console verification code is {####}." # EmailVerificationSubject: "Your Distribution Load Testing console verification code" # Policies: # PasswordPolicy: # MinimumLength: 8 # RequireLowercase: True # RequireNumbers: True # RequireSymbols: False # RequireUppercase: True # Schema: # - AttributeDataType: "String" # Name: "email" # Required: True # CognitoUserPoolClient: # Type: AWS::Cognito::UserPoolClient # Properties: # ClientName: !Sub ${AWS::StackName}-app # GenerateSecret: False # WriteAttributes: # - "address" # - "email" # - "phone_number" # RefreshTokenValidity: 1 # UserPoolId: !Ref CognitoUserPool # CognitoIdentityPool: # Type: AWS::Cognito::IdentityPool # Properties: # IdentityPoolName: !Sub ${AWS::StackName} # AllowUnauthenticatedIdentities: false # CognitoIdentityProviders: # - ClientId: !Ref CognitoUserPoolClient # ProviderName: !GetAtt CognitoUserPool.ProviderName # CognitoAttachRole: # Type: AWS::Cognito::IdentityPoolRoleAttachment # Properties: # IdentityPoolId: !Ref CognitoIdentityPool # Roles: # unauthenticated: !GetAtt CognitoUnAuthorizedRole.Arn # authenticated: !GetAtt CognitoAuthorizedRole.Arn # CognitoUser: # Type: AWS::Cognito::UserPoolUser # Properties: # DesiredDeliveryMediums: # - EMAIL # ForceAliasCreation: true # UserAttributes: # - Name: email # Value: !Ref AdminEmail # - Name: nickname # Value: !Ref AdminName # - Name: email_verified # Value: 'True' # Username: !Ref AdminName # UserPoolId: !Ref CognitoUserPool ## Step Functions TaskRunnerStepFunctions: Type: AWS::StepFunctions::StateMachine Properties: RoleArn: !GetAtt TaskRunnerStepFunctionsRole.Arn DefinitionString: !Sub | { "Comment": "Distributed Load Testing on AWS Task Runner", "StartAt": "Check running tests", "States": { "Check running tests": { "Type": "Task", "Resource": "${TaskStatusChecker.Arn}", "InputPath": "$", "OutputPath": "$", "Next": "No running test?" }, "No running test?": { "Type": "Choice", "Choices": [ { "Variable": "$.isRunning", "BooleanEquals": false, "Next": "Run tasks" } ], "Default": "Test is still running" }, "Test is still running": { "Type": "Fail", "Error": "TestAlreadyRunning", "Cause": "The same test is already running." }, "Run tasks": { "Type": "Task", "Resource": "${TaskRunner.Arn}", "InputPath": "$", "OutputPath": "$", "Next": "Wait 1 minute - task status" }, "Wait 1 minute - task status": { "Comment": "Wait 1 minute to check task status again", "Type": "Wait", "Seconds": 60, "Next": "Check task status" }, "Check task status": { "Type": "Task", "Resource": "${TaskStatusChecker.Arn}", "InputPath": "$", "OutputPath": "$", "Next": "All tasks done?" }, "All tasks done?": { "Type": "Choice", "Choices": [ { "Variable": "$.isRunning", "BooleanEquals": false, "Next": "Parse result" } ], "Default": "Wait 1 minute - task status" }, "Parse result": { "Type": "Task", "Resource": "${ResultsParser.Arn}", "Next": "Done" }, "Done": { "Type": "Pass", "End": true } } } Tags: - Key: SolutionId Value: SO0062 TaskRunnerStepFunctionsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: - sts:AssumeRole Path: /service-role/ Policies: - PolicyName: StepFunctionPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: - !GetAtt TaskStatusChecker.Arn - !GetAtt TaskRunner.Arn - !GetAtt ResultsParser.Arn # - !GetAtt EcrChecker.Arn ## Custom Resources CopyDockerFile: Type: Custom::CopyDockerFile Properties: ServiceToken: !GetAtt CustomResource.Arn Resource: CopyAssets # copy files based on https://solutions-us-east-1.s3.amazonaws.com/distributed-load-testing-on-aws/v1.2.0/container-manifest.json to ContainerBucket # container.zip SrcBucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] SrcPath: !FindInMap ["SourceCode", "General", "KeyPrefix"] ManifestFile: container-manifest.json DestBucket: !Ref ContainerBucket CopyConsoleFiles: Type: Custom::CopyConsoleFiles Properties: ServiceToken: !GetAtt CustomResource.Arn Resource: CopyAssets # copy files based on https://solutions-us-east-1.s3.amazonaws.com/distributed-load-testing-on-aws/v1.2.0/console-manifest.json to ConsoleBucket # [ # "console/favicon.ico", # "console/index.html", # "console/apple-icon.png", # "console/asset-manifest.json", # "console/static/js/main.d9b27963.chunk.js.map", # "console/static/js/2.619d7a16.chunk.js.LICENSE.txt", # "console/static/js/2.619d7a16.chunk.js.map", # "console/static/js/main.d9b27963.chunk.js", # "console/static/js/runtime-main.c33c8d33.js.map", # "console/static/js/2.619d7a16.chunk.js", # "console/static/js/runtime-main.c33c8d33.js", # "console/static/css/main.f8f730e1.chunk.css", # "console/static/css/2.b2b73f47.chunk.css", # "console/static/css/2.b2b73f47.chunk.css.map", # "console/static/css/main.f8f730e1.chunk.css.map", # "console/aws_config.js" empty # ] SrcBucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] SrcPath: !FindInMap ["SourceCode", "General", "KeyPrefix"] ManifestFile: console-manifest.json DestBucket: !Ref ConsoleBucket ConsoleConfig: Type: Custom::CopyConsoleFiles Properties: ServiceToken: !GetAtt CustomResource.Arn Resource: ConfigFile # copy ConfigFile below to ConsoleBucket as console/assets/aws_config.js DestBucket: !Ref ConsoleBucket AwsExports: !Sub | const awsConfig = { cw_dashboard: 'https://console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#dashboards:name=${EcsLoadTesting}', ecs_dashboard: 'https://${AWS::Region}.console.aws.amazon.com/ecs/home?region=${AWS::Region}#/clusters/${AWS::StackName}/tasks', aws_project_region: '${AWS::Region}', oauth: {}, aws_cloud_logic_custom: [ { name: 'dlts', endpoint: 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/prod', region: '${AWS::Region}' } ], aws_user_files_s3_bucket: '${ScenariosBucket}', aws_user_files_s3_bucket_region: '${AWS::Region}' } Uuid: Type: Custom::UUID Properties: ServiceToken: !GetAtt CustomResource.Arn Resource: UUID AnonymousMetric: Condition: Metrics Type: Custom::AnonymousMetric Properties: ServiceToken: !GetAtt CustomResource.Arn Resource: AnonymousMetric Region: !Ref AWS::Region SolutionId: SO0062 UUID: !GetAtt Uuid.UUID Version: v1.2.0 Outputs: Console: Description: Console URL Value: !Sub https://${ConsoleCloudFront.DomainName}/ ApiGatewayEndpoint: Description: "Distributed Load Testing API" Value: !Sub "https://${Api}.execute-api.${AWS::Region}.amazonaws.com/prod" SolutionUUID: Description: "Solution UUID" Value: !GetAtt Uuid.UUID