Capstone: a three-stage release pipeline with CodePipeline and CodeBuild

capstone · 80 min · Task statement 3.8

Task

Build, from one AWS CloudFormation template, a small but real continuous delivery pipeline: AWS CodePipeline takes a zip file from a versioned Amazon S3 bucket (the source stage), hands it to AWS CodeBuild to test and package (the build stage), and copies the result to a release folder in a second private bucket (the deploy stage). The pipeline and the build project act through two IAM roles, each allowed only what its job needs, and CodeBuild writes its logs to Amazon CloudWatch Logs. You release a first version, then release a broken one and watch the build stage stop it before it reaches the release folder, read the reason in the build log, and release a fixed version. The task draws on lesson 9 (IAM roles), lesson 12 (infrastructure as code), lesson 18 (S3 and versioning), lesson 7 (CloudWatch Logs) and this lesson's developer tools. At the end, deleting one stack and emptying two buckets removes every resource the lab created.

Steps

  1. Write the application. In CloudShell, make a working folder and a one-line release note:
mkdir -p clf-lab22 && cd clf-lab22
echo 'clf-lab22 release notes. Version 1' > release.txt

Then create buildspec.yml, the instructions CodeBuild follows. Open it with nano buildspec.yml, paste the text below, and save it (Ctrl+O, Enter, Ctrl+X):

version: 0.2
phases:
  build:
    commands:
      - grep -q Version release.txt
      - mkdir -p out
      - cp release.txt out/release.txt
      - date -u > out/built-at.txt
artifacts:
  base-directory: out
  files:
    - '**/*'

The grep line is the test: the build fails if the release note does not contain the word Version. The rest packages the note and a build time into the artefact. Zip both files:

zip app.zip buildspec.yml release.txt
  1. Write the infrastructure as a CloudFormation template. Open nano clf-lab22-pipeline.yaml, paste the template below and save it. If pasting into nano spoils the indentation, write the file on your computer instead and upload it with CloudShell's Actions, then Upload file, and move it into the folder with mv ~/clf-lab22-pipeline.yaml .
AWSTemplateFormatVersion: '2010-09-09'
Description: clf-lab22 capstone, a three-stage release pipeline
Resources:
  SourceBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'clf-lab22-source-${AWS::AccountId}'
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'clf-lab22-artifacts-${AWS::AccountId}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  BuildLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/codebuild/clf-lab22-build
      RetentionInDays: 1
  BuildRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: clf-lab22-codebuild-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: clf-lab22-codebuild-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt BuildLogs.Arn
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:PutObject
                Resource: !Sub '${ArtifactBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:GetBucketLocation
                  - s3:GetBucketAcl
                Resource: !GetAtt ArtifactBucket.Arn
  BuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: clf-lab22-build
      ServiceRole: !GetAtt BuildRole.Arn
      TimeoutInMinutes: 10
      Source:
        Type: CODEPIPELINE
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
      LogsConfig:
        CloudWatchLogs:
          Status: ENABLED
          GroupName: !Ref BuildLogs
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: clf-lab22-pipeline-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: clf-lab22-pipeline-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:PutObject
                  - s3:PutObjectAcl
                Resource:
                  - !Sub '${SourceBucket.Arn}/*'
                  - !Sub '${ArtifactBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:GetBucketVersioning
                  - s3:GetBucketLocation
                  - s3:ListBucket
                Resource:
                  - !GetAtt SourceBucket.Arn
                  - !GetAtt ArtifactBucket.Arn
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                Resource: !GetAtt BuildProject.Arn
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: clf-lab22-pipeline
      PipelineType: V2
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                S3Bucket: !Ref SourceBucket
                S3ObjectKey: app.zip
                PollForSourceChanges: 'false'
              OutputArtifacts:
                - Name: SourceOutput
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: '1'
              Configuration:
                ProjectName: !Ref BuildProject
              InputArtifacts:
                - Name: SourceOutput
              OutputArtifacts:
                - Name: BuildOutput
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                BucketName: !Ref ArtifactBucket
                Extract: 'true'
                ObjectKey: release
              InputArtifacts:
                - Name: BuildOutput

Read it before you deploy it: two private buckets, a log group, a build project, a pipeline with three stages, and two roles whose policies name only the buckets, the log group and the project they need. Nothing in it is public. 3. Deploy the template as a stack. CAPABILITY_NAMED_IAM is your explicit consent to it creating named IAM roles:

aws cloudformation deploy --template-file clf-lab22-pipeline.yaml --stack-name clf-lab22-stack --capabilities CAPABILITY_NAMED_IAM
aws cloudformation describe-stacks --stack-name clf-lab22-stack --query 'Stacks[0].StackStatus' --output text

The second command prints CREATE_COMPLETE. A new pipeline runs once by itself as soon as it exists, and this first run fails at the Source stage because app.zip is not in the bucket yet. That is expected; open the CodePipeline console and see that it shows exactly which stage failed. 4. Release version 1. Upload the zip to the source bucket and start the pipeline:

aws s3 cp app.zip s3://clf-lab22-source-111122223333/app.zip
aws codepipeline start-pipeline-execution --name clf-lab22-pipeline

Watch it move through the stages, re-running this every 20 seconds or so:

aws codepipeline get-pipeline-state --name clf-lab22-pipeline --query 'stageStates[].[stageName,latestExecution.status]' --output table

After a few minutes Source, Build and Deploy all read Succeeded. List the release:

aws s3 ls s3://clf-lab22-artifacts-111122223333/release/

It lists built-at.txt and release.txt. CodePipeline orchestrated the release; CodeBuild did the building. 5. Release a broken version. Change the release note so that it fails the test, rebuild the zip, upload it and start the pipeline again:

echo 'clf-lab22 release notes, draft' > release.txt
rm -f app.zip && zip app.zip buildspec.yml release.txt
aws s3 cp app.zip s3://clf-lab22-source-111122223333/app.zip
aws codepipeline start-pipeline-execution --name clf-lab22-pipeline

Watch the stages with the get-pipeline-state command from step 4. Source succeeds, Build reads Failed, and Deploy never runs, so the release folder still holds version 1. This is continuous integration doing its job: a bad change stopped at the build, not in front of users. 6. Find out why, from the build log in CloudWatch Logs:

aws logs tail /aws/codebuild/clf-lab22-build --since 15m

Near the end the log shows the grep -q Version release.txt command and that it did not exit successfully. The same log is linked from the failed build in the CodeBuild console, under Build history. A request that is slow across several services would need AWS X-Ray to trace it; a failed build needs only its log. 7. Release the fix:

echo 'clf-lab22 release notes. Version 2' > release.txt
rm -f app.zip && zip app.zip buildspec.yml release.txt
aws s3 cp app.zip s3://clf-lab22-source-111122223333/app.zip
aws codepipeline start-pipeline-execution --name clf-lab22-pipeline

When all three stages read Succeeded again, the release folder holds version 2. Because the source bucket is versioned, all three uploads of app.zip are still kept, which is how CodePipeline knows exactly which version each run used. 8. Open the CodePipeline console and choose clf-lab22-pipeline. The page draws the three stages and the latest run through them; Execution history lists every run, including the first failed Source run and the failed build. Then open the CodeBuild console, choose the project clf-lab22-build and look at Build history: one failed build between two that succeeded.

Verify

All three stages of the latest run succeeded:

aws codepipeline get-pipeline-state --name clf-lab22-pipeline --query 'stageStates[].latestExecution.status' --output text

Expected output: Succeeded three times, separated by tabs (Source, Build, Deploy).

The release folder holds the build's two files:

aws s3 ls s3://clf-lab22-artifacts-111122223333/release/

Expected output: two lines, one ending built-at.txt and one ending release.txt.

The source bucket kept every upload:

aws s3api list-object-versions --bucket clf-lab22-source-111122223333 --prefix app.zip --query 'length(Versions)'

Expected output: 3.

Cost and clean-up

CodeBuild and the V2 pipeline charge only while a build or an action is running, and the AWS Free Tier covers some build and pipeline minutes where your account is eligible. The buckets and the log group charge for what they store until they are deleted. Nothing here runs between your builds, but delete it all now rather than leave it in the account.

Created by this lab: the CloudFormation stack clf-lab22-stack; the pipeline clf-lab22-pipeline; the CodeBuild project clf-lab22-build; the IAM roles clf-lab22-codebuild-role and clf-lab22-pipeline-role; the CloudWatch Logs log group /aws/codebuild/clf-lab22-build; the versioned bucket clf-lab22-source-111122223333 with three versions of app.zip; the bucket clf-lab22-artifacts-111122223333 with the pipeline's artefacts and the release/ folder; the CloudShell folder clf-lab22.

  1. Empty the versioned source bucket. A versioned bucket keeps every version, so delete every version of app.zip, not just the current one:
aws s3api delete-objects --bucket clf-lab22-source-111122223333 --delete "$(aws s3api list-object-versions --bucket clf-lab22-source-111122223333 --query '{Objects: Versions[].{Key: Key, VersionId: VersionId}}' --output json)"

The S3 console's Empty button on the bucket does the same job, if you prefer it. 2. Empty the artefacts bucket, which holds the pipeline's artefacts and the release/ folder:

aws s3 rm s3://clf-lab22-artifacts-111122223333 --recursive
  1. Delete the stack, and wait for it to finish. This deletes everything the template made: the pipeline clf-lab22-pipeline, the project clf-lab22-build, the roles clf-lab22-codebuild-role and clf-lab22-pipeline-role, the log group /aws/codebuild/clf-lab22-build, and the now-empty buckets clf-lab22-source-111122223333 and clf-lab22-artifacts-111122223333:
aws cloudformation delete-stack --stack-name clf-lab22-stack
aws cloudformation wait stack-delete-complete --stack-name clf-lab22-stack

If the wait reports a failure, a bucket was not empty: repeat steps 1 and 2 and delete the stack clf-lab22-stack again. 4. Remove the local folder clf-lab22 from CloudShell:

cd ~ && rm -rf clf-lab22

Check it is gone

aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE ROLLBACK_COMPLETE DELETE_FAILED --query "length(StackSummaries[?StackName=='clf-lab22-stack'])"

Expected output: 0.

aws s3api list-buckets --query "length(Buckets[?starts_with(Name, 'clf-lab22-')])"

Expected output: 0.

aws codepipeline list-pipelines --query "length(pipelines[?name=='clf-lab22-pipeline'])"

Expected output: 0.

aws codebuild list-projects --query "length(projects[?@=='clf-lab22-build'])"

Expected output: 0.

aws iam list-roles --query "length(Roles[?starts_with(RoleName, 'clf-lab22-')])"

Expected output: 0.

aws logs describe-log-groups --log-group-name-prefix /aws/codebuild/clf-lab22 --query 'length(logGroups)'

Expected output: 0.

Notes

If the stack fails to create and rolls back, read the reason with aws cloudformation describe-stack-events --stack-name clf-lab22-stack --max-items 10, delete the stack clf-lab22-stack (a rolled-back stack must be deleted before the same name can be used again) and deploy it once more.

On a brand-new account, CodeBuild sometimes refuses to start any build because of an account-level limit on concurrent builds. The build then fails at once with a message about the number of builds allowed. That is a quota, not a mistake in the lab; the Service Quotas console shows it, and a quota increase request is free.

A real team would usually keep its code in a Git repository connected to CodePipeline rather than upload a zip to S3. S3 is used here because it needs no account outside AWS.

This is an independent study companion for the AWS Certified Cloud Practitioner (CLF-C02) exam and is not produced by or endorsed by Amazon Web Services. AWS, Amazon Web Services and AWS Certified Cloud Practitioner are trademarks of Amazon.com, Inc. or its affiliates.

This is an independent study companion for AWS Certified Cloud Practitioner CLF-C02 and is not produced by or endorsed by CompTIA.