AWSTemplateFormatVersion: '2010-09-09' Description: This Quick Start deploys CodeBuild, CodePipeline and CodeDeploy for .NET to an Elastic Container Service cluster (qs-1rs2qkq5d) Metadata: QuickStartDocumentation: EntrypointName: Launch into an existing ECS cluster Order: "2" AWS::CloudFormation::Interface: ParameterGroups: - Label: default: GitHub configuration Parameters: - GitHubRepositoryName - GitHubBranchName - GitHubOwner - GitHubOAuthToken - Label: default: Amazon ECS configuration Parameters: - EcsClusterName - EcsServiceName - TaskContainerName - Label: default: Build server configuration Parameters: - BuildServerAmiId - BuildServerInstanceType - BuildServerVolumeSize - BuildServerVolumeType - Label: default: Logging Parameters: - CloudWatchLogGroupName ParameterLabels: BuildServerInstanceType: default: Build server EC2 instance type BuildServerAmiId: default: Build server AMI ID BuildServerVolumeSize: default: Build server volume size BuildServerVolumeType: default: Build server volume type CloudWatchLogGroupName: default: CloudWatch log group name EcsClusterName: default: Amazon ECS cluster name EcsServiceName: default: Amazon ECS service name GitHubRepositoryName: default: Repository name GitHubBranchName: default: Branch to Build GitHubOwner: default: Project owner's GitHub user ID GitHubOAuthToken: default: OAuth token TaskContainerName: default: Task definition container name Parameters: BuildServerAmiId: Type: String Description: AMI ID or SSM Parameter expression to use to retrieve the AMI ID for the build server. Must be compatible with operating system version used by ECS cluster. Default: "{{ssm:/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-ECS_Optimized/image_id}}" BuildServerInstanceType: Type: String Description: Type of the EC2 instance on the build server. For more information, see https://aws.amazon.com/ec2/instance-types/[Amazon EC2 Instance Types^]. Default: t2.medium AllowedValues: [t2.small, t2.medium, t2.large, t3.small, t3.medium, t3.large, t3a.small, t3a.medium, t3a.large, c5.large, c5.xlarge, c5a.large, c5a.xlarge, c5ad.large, c5ad.xlarge, r5.large, r5.xlarge, r5a.large, r5a.xlarge, r5b.large, i3.large, i3.xlarge, d3.large, d3.xlarge] BuildServerVolumeSize: Type: Number Description: Size of EBS volume attached to the EC2 instance on the build server. Default: 100 BuildServerVolumeType: Type: String Description: Type of EBS volume attached to the EC2 instance on the build server. For more information, see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html[Amazon EBS volume types^]. Default: "gp2" AllowedValues: ["gp2", "gp3", "io1", "io2", "sc1", "st1", "standard"] CloudWatchLogGroupName: Type: String Description: Name of CloudWatch log group that receives logs from the EC2 instance on the build server. Default: /aws/quickstarts/dotnetfx-ecs-cicd EcsClusterName: Type: String Description: Name of the Amazon ECS cluster running the service that is the target of the CI/CD pipeline. EcsServiceName: Type: String Description: Name of the Amazon ECS service to which the Docker container image is deployed. MaxLength: 255 GitHubRepositoryName: Type: String Description: GitHub repository name Default: quickstart-dotnetfx-ecs-cicd GitHubBranchName: Type: String Description: GitHub branch name Default: main GitHubOwner: Type: String Description: GitHub user name of the GitHub repository owner. GitHubOAuthToken: Type: String NoEcho: true Description: OAuth token to use when authenticating with GitHub. CodePipeline uses the token to monitor the repository for changes. TaskContainerName: Type: String Description: The name of the container in a task definition where the Docker container image is deployed. MaxLength: 255 Resources: CodePipelineArtifactStoreBucket: Type: 'AWS::S3::Bucket' AppContainerRepo: Type: AWS::ECR::Repository Properties: RepositoryName: !Sub quickstart/demo-app-${GitHubRepositoryName} RepositoryPolicyText: Version: "2012-10-17" Statement: - Sid: AllowPullFromCodeBuild Effect: Allow Principal: Service: codebuild.amazonaws.com Action: - "ecr:GetDownloadUrlForLayer" - "ecr:BatchGetImage" - "ecr:BatchCheckLayerAvailability" CodePipelineServiceRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codepipeline.amazonaws.com Action: 'sts:AssumeRole' Path: / Policies: - PolicyName: !Sub AWS-CodePipeline-Service-${AWS::StackName} PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'iam:PassRole' Resource: '*' Effect: Allow Condition: StringEqualsIfExists: 'iam:PassedToService': - cloudformation.amazonaws.com - ec2.amazonaws.com - ecs-tasks.amazonaws.com - Action: - 'codestar-connections:UseConnection' Resource: '*' Effect: Allow - Action: - 'codedeploy:CreateDeployment' - 'codedeploy:GetApplication' - 'codedeploy:GetApplicationRevision' - 'codedeploy:GetDeployment' - 'codedeploy:GetDeploymentConfig' - 'codedeploy:RegisterApplicationRevision' Resource: '*' Effect: Allow - Action: - 'ec2:*' - 'cloudwatch:*' - 's3:*' - 'cloudformation:*' - 'ecs:*' Resource: '*' Effect: Allow - Action: - 'codebuild:BatchGetBuilds' - 'codebuild:StartBuild' Resource: '*' Effect: Allow - Effect: Allow Action: - 'ecr:DescribeImages' Resource: '*' AppPipeline: Type: 'AWS::CodePipeline::Pipeline' Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn ArtifactStore: Type: S3 Location: !Ref CodePipelineArtifactStoreBucket Stages: - Name: Source Actions: - Name: Source ActionTypeId: Category: Source Owner: ThirdParty Provider: GitHub Version: '1' RunOrder: 1 Configuration: Owner: !Ref GitHubOwner Repo: !Ref GitHubRepositoryName Branch: !Ref GitHubBranchName OAuthToken: !Ref GitHubOAuthToken PollForSourceChanges: false OutputArtifacts: - Name: SourceArtifact - Name: Build Actions: - Name: Build ActionTypeId: Category: Build Owner: AWS Provider: CodeBuild Version: '1' RunOrder: 1 Configuration: ProjectName: !Ref CodeBuildProject OutputArtifacts: - Name: BuildArtifact InputArtifacts: - Name: SourceArtifact - Name: Deploy Actions: - Name: Deploy ActionTypeId: Category: Deploy Owner: AWS Provider: ECS Version: '1' RunOrder: 1 Configuration: ClusterName: !Ref EcsClusterName ServiceName: !Ref EcsServiceName InputArtifacts: - Name: BuildArtifact CodeBuildServiceRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codebuild.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: !Sub aws-codebuild-service-${AWS::StackName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Resource: !Sub 'arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' Action: - 'ec2:RunInstances' Condition: StringEquals: "aws:RequestTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId - Effect: Allow Resource: !Sub 'arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' Action: - 'ec2:TerminateInstances' Condition: StringEquals: "aws:ResourceTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId - Effect: Allow Resource: '*' Action: - 'ec2:RunInstances' - 'ec2:DescribeInstances' - 'ec2:DescribeInstanceStatus' - 'ec2:CreateTags' - Effect: Allow Resource: - !Sub 'arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' Action: - 'ssm:SendCommand' Condition: StringEquals: "aws:ResourceTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId - Effect: Allow Resource: - !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunPowerShellScript' Action: - 'ssm:SendCommand' - Effect: Allow Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ContainerImageBuildServerRole} Action: - 'iam:PassRole' - Effect: Allow Resource: '*' Action: - 'ssm:CancelCommand' - 'ssm:DescribeAutomationExecutions' - 'ssm:DescribeInstanceInformation' - 'ssm:ListCommands' - 'ssm:ListCommandInvocations' - Effect: Allow Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-execution/ Action: - 'ssm:StopAutomationExecution' - 'ssm:GetAutomationExecution' - Effect: Allow Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${BuildDotNetContainerDocument}:$DEFAULT Action: - 'ssm:StartAutomationExecution' - Effect: Allow Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::parameter/aws/service/ami-windows-latest/* Action: - 'ssm:GetParameters' - Effect: Allow Resource: '*' Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' - Effect: Allow Resource: !Sub ${CodePipelineArtifactStoreBucket.Arn}* Action: - 's3:PutObject' - 's3:GetObject' - 's3:GetObjectVersion' - 's3:GetBucketAcl' - 's3:GetBucketLocation' CodeBuildProject: Type: AWS::CodeBuild::Project Properties: Source: Type: CODEPIPELINE InsecureSsl: false BuildSpec: !Sub | version: 0.2 env: variables: CONTAINER_NAME: ${TaskContainerName} DOCUMENT_NAME: ${BuildDotNetContainerDocument} ECR_REPO_URI: ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${AppContainerRepo} LOG_GROUP_NAME: ${CloudWatchLogGroupName} phases: pre_build: commands: - BUILD_ID=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8) - IMAGE_TAG=build-$BUILD_ID - SRC_ARTIFACT_FULL_S3_PATH=${!CODEBUILD_SOURCE_VERSION#*"s3:::"} - SRC_ARTIFACT_S3_PATH=${!SRC_ARTIFACT_FULL_S3_PATH#*"/"} - SRC_ARTIFACT_BUCKET_NAME=$(echo $SRC_ARTIFACT_FULL_S3_PATH | cut -c 1-$((${!#SRC_ARTIFACT_FULL_S3_PATH} - ${!#SRC_ARTIFACT_S3_PATH} - 1))) - echo SRC_ARTIFACT_FULL_S3_PATH=$SRC_ARTIFACT_FULL_S3_PATH - echo SRC_ARTIFACT_S3_PATH=$SRC_ARTIFACT_S3_PATH - echo SRC_ARTIFACT_BUCKET_NAME=$SRC_ARTIFACT_BUCKET_NAME build: commands: - echo Starting automation execution using document $DOCUMENT_NAME... - echo Logs are stored under CloudWatch Log Group $LOG_GROUP_NAME... - EXECUTION_ID=$(aws ssm start-automation-execution --document-name $DOCUMENT_NAME --parameters ImageTag=$IMAGE_TAG,PipelineBucketName=$SRC_ARTIFACT_BUCKET_NAME,SourceArtifactS3Path=$SRC_ARTIFACT_S3_PATH --output text) - echo Running execution $EXECUTION_ID... - COMMAND="aws ssm describe-automation-executions --filters Key=ExecutionId,Values=$EXECUTION_ID" - STATUS=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].AutomationExecutionStatus") - | while [ $STATUS = "InProgress" ]; do sleep 3; STATUS=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].AutomationExecutionStatus"); done - | if [ $STATUS = "Success" ]; then echo Automation execution succeeded.; else echo Automation execution failed. Please check CloudWatch log for details.; ERROR_MSG=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].FailureMessage"); echo SSM Failure Message Follows.; echo $ERROR_MSG; exit 1; fi - echo Writing image definition file... - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $ECR_REPO_URI:$IMAGE_TAG > imagedefinitions.json - cat imagedefinitions.json artifacts: files: - imagedefinitions.json Artifacts: Type: CODEPIPELINE Packaging: NONE EncryptionDisabled: false Cache: Type: NO_CACHE Environment: Type: LINUX_CONTAINER Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 ComputeType: BUILD_GENERAL1_SMALL PrivilegedMode: false ServiceRole: !GetAtt CodeBuildServiceRole.Arn TimeoutInMinutes: 60 QueuedTimeoutInMinutes: 480 LogsConfig: CloudWatchLogs: GroupName: !Ref CloudWatchLogGroupName Status: ENABLED S3Logs: Status: DISABLED ContainerImageBuildServerRole: Type : AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: s3:GetObject Resource: !Sub arn:${AWS::Partition}:s3:::${CodePipelineArtifactStoreBucket}/* Effect: Allow PolicyName: s3-instance-bucket-policy - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "ecr:GetAuthorizationToken" Resource: "*" Effect: Allow PolicyName: ecr-instance-policy - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "ecr:BatchCheckLayerAvailability" - "ecr:GetDownloadUrlForLayer" - "ecr:GetRepositoryPolicy" - "ecr:DescribeRepositories" - "ecr:ListImages" - "ecr:DescribeImages" - "ecr:BatchGetImage" - "ecr:GetLifecyclePolicy" - "ecr:GetLifecyclePolicyPreview" - "ecr:ListTagsForResource" - "ecr:DescribeImageScanFindings" - "ecr:InitiateLayerUpload" - "ecr:UploadLayerPart" - "ecr:CompleteLayerUpload" - "ecr:PutImage" Resource: !GetAtt AppContainerRepo.Arn Effect: Allow PolicyName: ecr-resource-instance-policy Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" - "ssm.amazonaws.com" Action: "sts:AssumeRole" ContainerImageBuildServerInstanceProfile: Type: "AWS::IAM::InstanceProfile" Properties: Roles: - !Ref ContainerImageBuildServerRole BuildDotNetContainerDocument: Type: AWS::SSM::Document Properties: DocumentType: Automation Content: schemaVersion: "0.3" description: "Create a .NET Framework Container for CodeBuild" parameters: AmiId: type: String default: !Ref BuildServerAmiId description: The Windows Server AMI to use for the build server must match version used by ECS cluster. InstanceType: type: String description: (Optional) The instance type of the EC2 instance to be launched. default: !Ref BuildServerInstanceType IamInstanceProfileName: type: String description: (Required) The IAM instance profile to attach to the build instance. default: !Ref ContainerImageBuildServerInstanceProfile InstanceVolumeSize: type: Integer description: (Required) Desired volume size (GiB) of the build instance. default: !Ref BuildServerVolumeSize InstanceVolumeType: type: String description: (Required) Desired volume type of the build instance. default: !Ref BuildServerVolumeType PipelineBucketName: type: String description: (Required) Bucket that build artifact is stored in. default: !Ref CodePipelineArtifactStoreBucket SourceArtifactS3Path: type: String description: (Required) Build artifact key. EcrRepoName: default: !Ref AppContainerRepo description: "ECR Repo Name" type: "String" ImageTag: type: String description: (Optional) Tag for container image. default: latest BuildLogGroupName: type: String description: (Optional) CloudWatch Log Group Name for build. default: !Ref CloudWatchLogGroupName mainSteps: - name: createNewInstance action: 'aws:runInstances' maxAttempts: 3 timeoutSeconds: 1200 onFailure: Abort inputs: ImageId: '{{ AmiId }}' InstanceType: '{{ InstanceType }}' MinInstanceCount: 1 MaxInstanceCount: 1 IamInstanceProfileName: '{{ IamInstanceProfileName }}' BlockDeviceMappings: - DeviceName: /dev/xvdb Ebs: VolumeSize: '{{ InstanceVolumeSize }}' VolumeType: '{{ InstanceVolumeType }}' TagSpecifications: - ResourceType: instance Tags: - Key: tag:aws:cloudformation:stack-id Value: !Ref AWS::StackId - Key: tag:aws:cloudformation:stack-name Value: !Ref AWS::StackName - Key: Name Value: dotnetfx-container-build nextStep: getInstance - name: getInstance action: 'aws:executeAwsApi' maxAttempts: 2 onFailure: Abort inputs: Service: ec2 Api: DescribeInstances InstanceIds: - '{{ createNewInstance.InstanceIds }}' outputs: - Name: InstanceId Selector: '$.Reservations[0].Instances[0].InstanceId' Type: String isCritical: 'true' nextStep: waitForInstanceToBeReady - name: waitForInstanceToBeReady action: 'aws:waitForAwsResourceProperty' onFailure: 'step:terminateInstance' timeoutSeconds: 600 maxAttempts: 2 inputs: Service: ec2 Api: DescribeInstanceStatus InstanceIds: - '{{ getInstance.InstanceId }}' PropertySelector: '$.InstanceStatuses[0].InstanceStatus.Details[0].Status' DesiredValues: - passed isCritical: 'false' nextStep: waitForSSMAgentOnline - name: waitForSSMAgentOnline action: 'aws:waitForAwsResourceProperty' onFailure: 'step:terminateInstance' timeoutSeconds: 600 inputs: Service: ssm Api: DescribeInstanceInformation InstanceInformationFilterList: - key: InstanceIds valueSet: - '{{ getInstance.InstanceId }}' PropertySelector: '$.InstanceInformationList[0].PingStatus' DesiredValues: - Online isCritical: 'true' nextStep: CreateDockerImage - name: CreateDockerImage action: 'aws:runCommand' onFailure: 'step:terminateInstance' timeoutSeconds: 7200 inputs: DocumentName: AWS-RunPowerShellScript InstanceIds: - '{{ getInstance.InstanceId }}' Parameters: commands: | $ErrorActionPreference = "Stop" Write-Host "*****Downloading from S3...*****" New-Item -Name tmp -ItemType directory -Force | out-null New-Item -Name tmp\src -ItemType directory -Force | out-null cd .\tmp $tmpFolder = $(Get-Location).Path $srcFolder = "$tmpFolder\src" $key = "{{SourceArtifactS3Path}}" $srcFileName = "$(Split-Path $key -Leaf)" Read-S3Object -BucketName "{{PipelineBucketName}}" -Key $key -File $srcFileName Expand-Archive -Path "$tmpFolder\$srcFileName" -DestinationPath "$srcFolder" -Force Write-Host "*****Building Docker image...*****" $instanceInfo = (Invoke-RestMethod -Method Get -Uri http://169.254.169.254/latest/dynamic/instance-identity/document) $repoUri = $instanceInfo.accountId + '.dkr.ecr.' + $instanceInfo.region + '.amazonaws.com/{{EcrRepoName}}' $latestUri = "$($repoUri):latest" $hasCustomTag = $False $buildArgs = @("-t", $latestUri) $customTag = "{{ImageTag}}" if ($customTag -and $($customTag -ne "latest")) { $hasCustomTag = $True $uriWithTag = "$($repoUri):$customTag" $buildArgs += @("-t", $uriWithTag) } Write-Host "Docker Build Args are '$buildArgs'" cd $srcFolder docker build $buildArgs $srcFolder\ if ($LASTEXITCODE -ne 0) { throw ("'docker build $buildArgs $srcFolder\' execution failed with exit code $LASTEXITCODE.") } Write-Host "*****Pushing Docker image to ECR...*****" Invoke-Expression –Command (Get-ECRLoginCommand –Region $region).Command docker push $repoUri --all-tags if ($LASTEXITCODE -ne 0) { throw "'docker push $repoUri --all-tags' execution failed with exit code $LASTEXITCODE." } executionTimeout: '7200' CloudWatchOutputConfig: CloudWatchLogGroupName: '{{ BuildLogGroupName }}' CloudWatchOutputEnabled: true isCritical: 'true' nextStep: terminateInstance - name: terminateInstance action: 'aws:changeInstanceState' maxAttempts: 3 onFailure: Continue inputs: InstanceIds: - '{{getInstance.InstanceId}}' DesiredState: terminated isCritical: 'true' isEnd: 'true' Outputs: CodePipelineName: Description: CodePipeline Name Value: !Ref AppPipeline Postdeployment: Description: See the deployment guide for post-deployment steps. Value: https://aws.amazon.com/quickstart/?quickstart-all.sort-by=item.additionalFields.sortDate&quickstart-all.sort-order=desc&awsm.page-quickstart-all=5