How To: CodeBuild with Docker Image using AWS CloudFormation
AWS offers CI/CD tools that enable you to build and deploy your application continuously. Under Developer Tools, you will find CodeCommit, CodeBuild, CodeDeploy, and CodePipeline. CodePipeline ties all of these tools together by allowing your application to built and deployed upon code change in your CodeCommit repository. This blog will focus on how to configure your CodeBuild project to build a Docker image for your application as part of the CICD pipeline. I chose to use CloudFormation to write the CodeBuild project.
Parameters and Resources below should be put in one file such as cicd.yaml
.
Parameters (cicd.yaml)
In the CodeBuild project that I created,ServiceName
refers to the CodeCommit repository name, where it pulls code from.
Parameters:
Environment:
Description: 'The env name of this stack, default is ''dev'''
Default: dev
Type: String
AllowedValues:
- dev
- stg
- uat
- prodServiceName:
Description: String text of service name
Default: $PROJECT_NAME
Type: String
Resources (cicd.yaml)
1. CodeBuildServiceRole
This role is assigned to the CodeBuild project.
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${ServiceName}-codebuild-service-role-${Environment}
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- codebuild.amazonaws.com
2. CodeBuildServicePolicy
This policy is attached to the role created above.
CodeBuildServicePolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub ${ServiceName}-CodeBuildServicePolicy-${Environment}
Roles:
- !Ref CodeBuildServiceRole
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "arn:aws:logs:*:*:*"
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource:
- !Sub "arn:aws:s3:::codepipeline-${AWS::Region}-*/*"
- Effect: Allow
Action:
- codecommit:GitPull
Resource: !Sub "arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${ServiceName}"
- Effect: Allow
Action:
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:CompleteLayerUpload
- ecr:InitiateLayerUpload
- ecr:PutImage
- ecr:UploadLayerPart
Resource: "*"
- Effect: Allow
Action:
- ecs:RegisterTaskDefinition
Resource: "*"
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
Resource: !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${ServiceName}-${Environment}-*"
- Effect: Allow
Action:
- iam:PassRole
Resource: !GetAtt CodeBuildServiceRole.Arn
- Effect: Allow
Action:
- ec2:DescribeSecurityGroups
- ec2:DescribeVpcs
- ec2:DescribeSubnets
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
- ec2:CreateNetworkInterface
- ec2:DescribeDhcpOptions
Resource: "*"
- Effect: Allow
Action:
- ec2:CreateNetworkInterfacePermission
Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*"
Condition:
StringLike:
ec2:Subnet:
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/*"
ec2:AuthorizedService: codebuild.amazonaws.com
3. CodeBuildProjectEnvironmentVariables
can be referenced in buildspec.yml
. PrivilegedMode
needs to be set to true
for the CodeBuild project to be able to push a Docker image to ECR.
If your project uses a private npm registry, you will need to run your CodeBuild project in a VPC that is allowed to make requests to the private npm registry. You can use the VpcConfig property to set this up.
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${ServiceName}-${Environment}
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Artifacts:
Type: NO_ARTIFACTS
Environment:
Type: linuxContainer
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:2.0
PrivilegedMode: true
EnvironmentVariables:
- Name: CURRENT_ENV
Type: PLAINTEXT
Value: !Ref Environment
- Name: ACCOUNT_ID
Type: PLAINTEXT
Value: !Ref 'AWS::AccountId'
- Name: SERVICE_NAME
Type: PLAINTEXT
Value: !Ref ServiceName
Source:
Type: CODECOMMIT
Location: !Sub https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${ServiceName}
SourceVersion: refs/heads/master
TimeoutInMinutes: 10
buildspec.yml
The build specification file (buildspec.yml) should be placed in the root of your source directory, as CodeBuild will search for it there by default. If you want to change the name and location of the buildspec file, then you can specify it in the Source property of the CodeBuild project. This project assumes that the ECR repository is named in the format $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/$SERVICE_NAME-$CURRENT_ENV
.
This buildspec file will build a Docker image, push it to the ECR, create a new task definition referencing the newly created Docker image, and create an appspec file used by CodeDeploy, which is the next phase of the CICD pipeline that will deploy your application.
sed
is used to replace variables in the task definition and appspec file outputted by this CodeBuild project.
version: 0.2phases:
install:
runtime-versions:
docker: 18
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
- $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
- echo ACCOUNT_ID=$ACCOUNT_ID
- echo SERVICE_NAME=$SERVICE_NAME
- echo CURRENT_ENV=$CURRENT_ENV
- REPOSITORY_URI=$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/$SERVICE_NAME-$CURRENT_ENV
- IMAGE_TAG=version_$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- echo IMAGE_TAG=$IMAGE_TAG
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker push $REPOSITORY_URI:$IMAGE_TAG # Create a valid json file that will be used to create a new task definition version
# Using sed we need to replace variables
- echo Creating a task definition json
- sed "s+\$APP_IMAGE+$REPOSITORY_URI:$IMAGE_TAG+g; s+\$SERVICE_NAME+$SERVICE_NAME+g; s+\$CURRENT_ENV+$CURRENT_ENV+g; s+\$ACCOUNT_ID+$ACCOUNT_ID+g;" taskdef.json > register-task-definition.json
# Using the aws cli we need to register a new task definition to create a valid appspec.yaml
- echo Creating an appspec.yaml file
- TASK_DEFINITION_ARN=`aws ecs register-task-definition --cli-input-json "$(cat register-task-definition.json)" --query 'taskDefinition.taskDefinitionArn' --output text`
- sed "s+\$TASK_DEFINITION_ARN+$TASK_DEFINITION_ARN+g; s+\$SERVICE_NAME+$SERVICE_NAME+g; s+\$CURRENT_ENV+$CURRENT_ENV+g;" appspec.yml > appspec.yamlartifacts:
files:
- appspec.yaml
- register-task-definition.jsondiscard-paths: yes
appspec.yml
appspec.yml
should be placed in the root of your source directory. It is used as a template to create an appspec.yaml
file in CodeBuild that is later used in CodeDeploy, which is the next phase of the CICD pipeline. It will be used to run a new task in your ECS cluster with the task definition created by the CodeBuild project.
ContainerName
should be set to the ECS cluster name in which your application is running.
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "$TASK_DEFINITION_ARN"
LoadBalancerInfo:
ContainerName: "$SERVICE_NAME-$CURRENT_ENV-cluster"
ContainerPort: 8080
taskdef.json
taskdef.json
should be placed in the root of your source directory.
$ROLE_NAME should be replaced with the IAM role assigned to your ECS service task.
{
"containerDefinitions": [
{
"essential": true,
"image": "$APP_IMAGE",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group" : "$SERVICE_NAME-$CURRENT_ENV-ecs-log-group",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "$SERVICE_NAME"
}
},
"name": "$SERVICE_NAME",
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{
"name": "env",
"value": "$CURRENT_ENV"
},
{
"name": "NODE_ENV",
"value": "$CURRENT_ENV"
},
{
"name": "NUXT_ENV",
"value": "$CURRENT_ENV"
}
]
}
],
"cpu": "512",
"executionRoleArn":
"arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME",
"taskRoleArn": "arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME",
"family": "$ROLE_NAME-task",
"memory": "2048",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
]
}