DEV Community

Cover image for Using CloudFormation to Automate Build, Test, and Deploy with CodePipeline
Jenna Pederson for AWS

Posted on • Originally published at jennapederson.com

Using CloudFormation to Automate Build, Test, and Deploy with CodePipeline

In part 1, we automated the provisioning of an Amazon EC2 instance using AWS CloudFormation. In part 2, we added an Amazon RDS Postgresql database to the CloudFormation template from part 1 so both the EC2 instance and the database can be provisioned together as a set of resources. Today in part 3, we will introduce a continuous integration/continuous deployment (CI/CD) pipeline to automate the build, test, and deploy phases of your release process. To do this, we’ll use AWS CodeDeploy and CodePipeline.

As a reminder, here's what we've covered and where we're going:

  • automate the provisioning of your EC2 instance using CloudFormation (part 1),
  • add an RDS Postgresql database to your stack with CloudFormation (part 2), and
  • create a CodePipeline with CloudFormation (this post, part 3).

Prerequisites

To work through the examples in this post, you’ll need:

  • an AWS account (you can create your account here if you don’t already have one),
  • the AWS CLI installed (you can find instructions for installing the AWS CLI here), and
  • a key-pair to use for SSH (you can create a key-pair following these instructions).

Unfamiliar with CloudFormation or feeling a little rusty? Check out part 1 or my Intro to CloudFormation post before getting started.

Just want the code? Grab it here and then check out the buildspec, appspec, and scripts here.

Prepare EC2 Instance

First, we’ll need to prepare the EC2 instance so that we can deploy our app to it. We’ll create an EC2 instance role and a CodeDeploy trust role, install the CodeDeploy agent, and tag the instance or instance we want to deploy to.

1. Create EC2 Instance Role

In the CloudFormation template that creates your EC2 instance, create the following new resources, InstanceRole, InstanceRolePolicies, and InstanceRoleInstanceProfile:

InstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / InstanceRolePolicies: Type: AWS::IAM::Policy Properties: PolicyName: InstanceRole PolicyDocument: Statement: - Effect: Allow Action: - autoscaling:Describe* - cloudformation:Describe* - cloudformation:GetTemplate - s3:Get* Resource: '*' Roles: - !Ref 'InstanceRole' InstanceRoleInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref 'InstanceRole' 
Enter fullscreen mode Exit fullscreen mode

These resources create an instance profile to pass an IAM role to an EC2 instance. This allows the EC2 instance to do things like get the CodeDeploy agent from S3.

Next, we’ll add the IamInstanceProfile property to the EC2 instance:

WebAppInstance: Properties: ... IamInstanceProfile: !Ref 'InstanceRoleInstanceProfile' 
Enter fullscreen mode Exit fullscreen mode

2. Install CodeDeploy Agent

The CodeDeploy agent will be installed on each EC2 instance you want to deploy your app to and helps CodeDeploy communicate with your EC2 instance for deployments. First, you’ll include metadata in the AWS::CloudFormation::Init key, which will be used by the cfn-init helper script.

WebAppInstance: ... Metadata: AWS::CloudFormation::Init: services: sysvint: codedeploy-agent: enabled: 'true' ensureRunning: 'true' 
Enter fullscreen mode Exit fullscreen mode

Then, we’ll add the UserData key, which allows us to pass user data to the EC2 instance to perform automated configuration tasks and run scripts after the instance starts up.

WebAppInstance: Properties: ... UserData: !Base64 Fn::Join: - '' - - "#!/bin/bash -ex\n" - "yum update -y aws-cfn-bootstrap\n" - "yum install -y aws-cli\n" - "yum install -y ruby\n" - "iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000\n" - "echo 'iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000' >> /etc/rc.local\n" - "# Helper function.\n" - "function error_exit\n" - "{\n" - ' /opt/aws/bin/cfn-signal -e 1 -r "$1" ''' - !Ref 'WaitHandle' - "'\n" - " exit 1\n" - "}\n" - "# Install the AWS CodeDeploy Agent.\n" - "cd /home/ec2-user/\n" - "aws s3 cp 's3://aws-codedeploy-us-east-1/latest/codedeploy-agent.noarch.rpm'\  \ . || error_exit 'Failed to download AWS CodeDeploy Agent.'\n" - "yum -y install codedeploy-agent.noarch.rpm || error_exit 'Failed to\  \ install AWS CodeDeploy Agent.' \n" - '/opt/aws/bin/cfn-init -s ' - !Ref 'AWS::StackId' - ' -r WebAppInstance --region ' - !Ref 'AWS::Region' - " || error_exit 'Failed to run cfn-init.'\n" - "# All is well, so signal success.\n" - /opt/aws/bin/cfn-signal -e 0 -r "AWS CodeDeploy Agent setup complete." ' - !Ref 'WaitHandle' - "'\n" 
Enter fullscreen mode Exit fullscreen mode

This installs a few helper packages like the aws-cli and aws-cfn-bootstrap, and then installs the CodeDeploy agent (by copying it from S3). The cfn-init script grabs the metadata we added earlier and ensures those services are enabled and running. The cfn-signal helper script signals to CloudFormation that the instance had been successfully created or updated.

Finally, add the following two resources that are used in the UserData we just added so that CloudFormation waits until the UserData scripts are finished running.

WaitHandle: Type: AWS::CloudFormation::WaitConditionHandle WaitCondition: Type: AWS::CloudFormation::WaitCondition Properties: Handle: !Ref 'WaitHandle' Timeout: '900' 
Enter fullscreen mode Exit fullscreen mode

3. Tag the Instances

Next, we need to tag the EC2 instances. CodeDeploy will use these tags to identify which instances to deploy to.

WebAppInstance: Properties: ... Tags: - Key: 'CodeDeployTag' Value: 'CodeDeployDemo' 
Enter fullscreen mode Exit fullscreen mode

4. Create CodeDeploy Trust Role

We also need to add a CodeDeploy trust role so that CodeDeploy has access to work with the EC2 instance. Add the following two resources:

CodeDeployTrustRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Sid: '1' Effect: Allow Principal: Service: - codedeploy.us-east-1.amazonaws.com - codedeploy.us-west-2.amazonaws.com Action: sts:AssumeRole Path: / CodeDeployRolePolicies: Type: AWS::IAM::Policy Properties: PolicyName: CodeDeployPolicy PolicyDocument: Statement: - Effect: Allow Resource: - '*' Action: - ec2:Describe* - Effect: Allow Resource: - '*' Action: - autoscaling:CompleteLifecycleAction - autoscaling:DeleteLifecycleHook - autoscaling:DescribeLifecycleHooks - autoscaling:DescribeAutoScalingGroups - autoscaling:PutLifecycleHook - autoscaling:RecordLifecycleActionHeartbeat Roles: - !Ref 'CodeDeployTrustRole' 
Enter fullscreen mode Exit fullscreen mode

This trust role will be used in the next section when we configure the CodePipeline. We’ll pass the ARN of this trust role to that CloudFormation template, so let’s add this as an Output:

Outputs: ... CodeDeployTrustRoleARN: Value: !GetAtt 'CodeDeployTrustRole.Arn' 
Enter fullscreen mode Exit fullscreen mode

5. Create the stack

And finally, we’ll need to create the stack:

aws cloudformation create-stack --stack-name CloudFormationEc2Example --template-body file://07_ec2_codedeploy.yaml \ --parameters ParameterKey=AvailabilityZone,ParameterValue=us-east-1a \ ParameterKey=EnvironmentType,ParameterValue=dev \ ParameterKey=KeyPairName,ParameterValue=jenna \ ParameterKey=DBPassword,ParameterValue=Abcd1234 \ --capabilities CAPABILITY_IAM 
Enter fullscreen mode Exit fullscreen mode

Or, if you’re updating the stack you created in part 2, you can use the update-stack command instead.

Once we’ve created the EC2 instance set up for CodeDeploy, we’ll be ready to create the CodePipeline pipeline.

To view the full version of this template, check it out here.

Create CodePipeline pipeline

The pipeline we are building will have three stages:

  • A Source stage to pull our code from the GitHub repository. The Source stage will use a CodeStarConnection and an S3 bucket.

  • A Build stage to build the source code into an artifact. The Build stage will use a CodeBuild Project and the same S3 bucket.

  • A Deploy stage to deploy the artifact to the EC2 instance. The Deploy stage will use a CodeDeploy Application and a CodeDeploy DeploymentGroup.

First, we’ll need an app to deploy.

1. Prepare the App to Deploy

You can fork the hello-express repo into your own github account. This is a simple Node/Express web app. For the purposes of this demo, the most interesting parts are the buildspec.yml and appspec.yml files, and scripts in the bin directory:

  • The buildspec.yml file is a specification file that contains build commands and configuration that are used to build a CodeBuild project.
  • The appspec.yml file is a specification file that defines a series of lifecycle hooks for a CodeDeploy deployment.
  • The bin directory contains the www start script for the app, and the scripts for each of the lifecycle hooks. You can read more about the lifecycle hooks here.

2. Add Parameters

Then, we’ll need the following input Parameters for our template:

Parameters: GitHubRepo: Type: String GitHubBranch: Type: String Default: main GitHubUser: Type: String CodeDeployServiceRole: Type: String Description: A service role ARN granting CodeDeploy permission to make calls to EC2 instances with CodeDeploy agent installed. TagKey: Description: The EC2 tag key that identifies this as a target for deployments. Type: String Default: CodeDeployTag AllowedPattern: '[\x20-\x7E]*' ConstraintDescription: Can contain only ASCII characters. TagValue: Description: The EC2 tag value that identifies this as a target for deployments. Type: String Default: CodeDeployDemo AllowedPattern: '[\x20-\x7E]*' ConstraintDescription: Can contain only ASCII characters. 
Enter fullscreen mode Exit fullscreen mode

3. Create Service Roles

In order for CodeBuild to access S3 to put the built artifact into the bucket, we'll need to create a service role, CodeBuildServiceRole. We’ll need a second service role, CodePipelineServiceRole, which allows CodePipeline to get the source code from the GitHub connection, to start builds, to get the artifact from the bucket, and to create and deploy the app. Add these two IAM resources:

CodeBuildServiceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: "logs" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - ecr:GetAuthorizationToken - ssm:GetParameters Resource: "*" - PolicyName: "S3" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - s3:GetObject - s3:PutObject - s3:GetObjectVersion Resource: !Sub arn:aws:s3:::${ArtifactBucket}/* CodePipelineServiceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* - !Sub arn:aws:s3:::${ArtifactBucket} Effect: Allow Action: - s3:* - Resource: "*" Effect: Allow Action: - codebuild:StartBuild - codebuild:BatchGetBuilds - iam:PassRole - Resource: - !Ref CodeStarConnection Effect: Allow Action: - codestar-connections:UseConnection - Resource: "*" Effect: Allow Action: - codedeploy:CreateDeployment - codedeploy:CreateDeploymentGroup - codedeploy:GetApplication - codedeploy:GetApplicationRevision - codedeploy:GetDeployment - codedeploy:GetDeploymentConfig - codedeploy:RegisterApplicationRevision 
Enter fullscreen mode Exit fullscreen mode

4. Create the Source Stage

For the Source stage, we’ll create a CodeStar Connection and an S3 Bucket.

1. Create a CodeStarConnection

When we set up the pipeline, we’ll have a Source stage the pulls our source code from GitHub. To do this, we’ll need to create a CodeStarConnection for GitHub. This will give our pipeline access to a GitHub repository. We’ll use CloudFormation to create this by adding the resource to our template, but there will be a manual step to change the connection from Pending to Available after the first time we apply the template.

CodeStarConnection: Type: 'AWS::CodeStarConnections::Connection' Properties: ConnectionName: CfnExamplesGitHubConnection ProviderType: GitHub 
Enter fullscreen mode Exit fullscreen mode

2. Create S3 Bucket to Hold Artifacts

We’ll need a place to store the build artifacts so we’ll create an S3 bucket. Add the following S3 resource to your template:

ArtifactBucket: Type: AWS::S3::Bucket DeletionPolicy: Delete 
Enter fullscreen mode Exit fullscreen mode

3. Create the Stage

Then we’ll create the first stage of the pipeline.

Pipeline: Type: AWS::CodePipeline::Pipeline Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn ArtifactStore: Type: S3 Location: !Ref ArtifactBucket Stages: - Name: Source Actions: - Name: App ActionTypeId: Category: Source Owner: AWS Version: '1' Provider: CodeStarSourceConnection Configuration: ConnectionArn: !Ref CodeStarConnection BranchName: !Ref GitHubBranch FullRepositoryId: !Sub ${GitHubUser}/${GitHubRepo} OutputArtifacts: - Name: AppArtifact RunOrder: 1 
Enter fullscreen mode Exit fullscreen mode

Using the CodeStarSourceConnection resource we created above, this will configure it to use the branch, GitHub user, and repository name based on the input parameters.

5. Create Build Stage

For the Build stage, we’ll create a CodeBuild Project that indicates what kind of environment to build the code in. Here, we’re using a Docker Linux container. We’ll also set the service role to the service role we created earlier.

CodeBuildProject: Type: AWS::CodeBuild::Project Properties: Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: buildspec.yml Environment: ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/docker:17.09.0 Type: LINUX_CONTAINER Name: !Ref AWS::StackName ServiceRole: !Ref CodeBuildServiceRole 
Enter fullscreen mode Exit fullscreen mode

Then we need to add the Build stage to the pipeline we started in the last step.

- Name: Build Actions: - Name: Build ActionTypeId: Category: Build Owner: AWS Version: '1' Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProject InputArtifacts: - Name: AppArtifact OutputArtifacts: - Name: BuildOutput RunOrder: 1 
Enter fullscreen mode Exit fullscreen mode

This will also use the same S3 bucket from a the previous step to store the build output.

6. Create Deploy Stage

In the last stage, Deploy, we’ll need a CodeDeploy Application and a CodeDeploy DeploymentGroup.

CodeDeployApplication: Type: AWS::CodeDeploy::Application CodeDeployGroup: Type: AWS::CodeDeploy::DeploymentGroup Properties: ApplicationName: !Ref CodeDeployApplication Ec2TagFilters: - Key: !Ref 'TagKey' Value: !Ref 'TagValue' Type: KEY_AND_VALUE ServiceRoleArn: !Ref 'CodeDeployServiceRole' 
Enter fullscreen mode Exit fullscreen mode

The DeploymentGroup uses the EC2TagFilters to specify which group of EC2 instances to deploy to. When we setup the EC2 instance above, we tagged it with a tag/value that is used here. We also set the service role to the one we created earlier.

Then we add the final stage to the pipeline.

- Name: Deploy Actions: - Name: Deploy ActionTypeId: Category: Deploy Owner: AWS Version: '1' Provider: CodeDeploy Configuration: ApplicationName: !Ref CodeDeployApplication DeploymentGroupName: !Ref CodeDeployGroup InputArtifacts: - Name: BuildOutput RunOrder: 1 
Enter fullscreen mode Exit fullscreen mode

7. Add Outputs

Last, we need to add an Output to our template to give us the fully URL to view our pipeline in the AWS Console.

Outputs: PipelineUrl: Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline} 
Enter fullscreen mode Exit fullscreen mode

To view the full version of this template, check it out here.

8. Create the Stack

Now that we have the pipeline template created, we can create the stack. We’ll need to copy CodeDeployTrustRoleARN Output from previous EC2 stack, which you can grab from the Outputs tab in the AWS Console.

CloudFormation Stack Outputs tab in the AWS Console

Then, run create-stack at the command line, replacing CODE_DEPLOY_SERVICE_ROLE_ARN below with the ARN you just copied.

aws cloudformation create-stack --stack-name CloudFormationPipelineExample --template-body file://08_pipeline.yaml \ --parameters ParameterKey=GitHubRepo,ParameterValue=hello-express \ ParameterKey=GitHubUser,ParameterValue=jennapederson \ ParameterKey=CodeDeployServiceRole,ParameterValue=CODE_DEPLOY_SERVICE_ROLE_ARN \ --capabilities CAPABILITY_IAM 
Enter fullscreen mode Exit fullscreen mode

Because this template creates IAM roles, we also need to tell CloudFormation that this capability (creating IAM resources) is allowed to be used by specifying the --capabilities option.

9. Make CodeStarConnection Available

Once the stack is created successfully, you'll need to change the CodeStarConnection from Pending to Available. To do this, head over to the AWS Console and find your newly created stack. On the Outputs tab, click the link to go to your new pipeline.

CloudFormation Stack Outputs tab in the AWS Console

In the left-hand menu under Settings, click Connections. Select the new connection and click the "Update pending connection" button.

CodeStar Connection Settings

You'll need to give GitHub access to your account and the repository before the connection will become available. You can read more about that here in the section "To create a connection to GitHub."

10. Retry the Source Stage

Now that your CodeStarConnection is Available, head back to your pipeline and note that the Source stage has failed because of the Pending CodeStarConnection.

CodePipeline pipeline showing a Source stage failure

Click the "Retry" button next to the Source stage to restart it.

CodePipeline pipeline showing the Retry button for the failed Source stage

Your pipeline will restart by pulling the source code from GitHub, build the app, and then deploy the artifact to your EC2 instance! When complete, you can grab the WebServerPublicDNS URL from your EC2 stack and open it in a browser:

CloudFormation Stack Outputs tab in the AWS Console

And you will see Hello, Express show in your browser!

The Express app running in the browser

What you learned

In this post, we enhanced the CloudFormation template from part 2 to install CodeDeploy agent and tag the EC2 instances you want to deploy to. Then we created a new template that sets up a Source, Build, and Deploy stage for a CodePipeline complete with a CodeStarConnection, CodeBuild project, and a CodeDeploy application. Now you have a pipeline that will pull source code, build your app, and deploy it to EC2 when there are changes to a specific branch in a GitHub repository.

You can grab the final CloudFormation template we created here.

Like what you read? Follow me here on Dev.to or on Twitter to stay updated!

Top comments (0)