Capstone: a three-stage release pipeline with CodePipeline and CodeBuild
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
- 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
- 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 withmv ~/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.
- 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
- Delete the stack, and wait for it to finish. This deletes everything the template made: the pipeline
clf-lab22-pipeline, the projectclf-lab22-build, the rolesclf-lab22-codebuild-roleandclf-lab22-pipeline-role, the log group/aws/codebuild/clf-lab22-build, and the now-empty bucketsclf-lab22-source-111122223333andclf-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.