Automatically push test images to ECR using CFN custom resources and CodeBuild
Consider automatically pushing test images to an ECR repository when creating a CloudFormation stack.
In this article, we will show you how to achieve the above using CloudFormation custom resources and CodeBuild.
Environment
Create four resources in CloudFormation.
Secrets Manager is used to store DockerHub account information.
Register account name and password in JSON format.
Build a Docker image for testing with CodeBuild.
The image will be based on the one pulled from DockerHub.
Push the built image to the ECR repository.
Create a Lambda function.
The function’s action is to start CodeBuild.
The runtime environment for the function is Python 3.8.
Associate this function with a CloudFormation custom resource so that it is automatically executed when the CloudFormation stack is created.
CloudFormation template files
The above configuration is built with CloudFormation.
The CloudFormation templates are placed at the following URL
https://github.com/awstut-an-r/awstut-fa/tree/main/129
Explanation of key points of template files
ECR
Resources:
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Ref Prefix
Code language: YAML (yaml)
Create an ECR repository.
No special configuration is required.
Secrets Manager
Resources:
Secret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Ref Prefix
SecretString: !Sub '{"username":"${Username}","password":"${Password}"}'
Code language: YAML (yaml)
Save your DockerHub account information in Secrets Manager.
For more information on Secrets Manager, please also check here.
CodeBuild
Resources:
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: NO_ARTIFACTS
Cache:
Type: NO_CACHE
Environment:
ComputeType: !Ref ProjectEnvironmentComputeType
EnvironmentVariables:
- Name: DOCKERHUB_PASSWORD
Type: SECRETS_MANAGER
Value: !Sub "${Secret}:password"
- Name: DOCKERHUB_USERNAME
Type: SECRETS_MANAGER
Value: !Sub "${Secret}:username"
Image: !Ref ProjectEnvironmentImage
ImagePullCredentialsType: CODEBUILD
Type: !Ref ProjectEnvironmentType
PrivilegedMode: true
LogsConfig:
CloudWatchLogs:
Status: DISABLED
S3Logs:
Status: DISABLED
Name: !Ref Prefix
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: NO_SOURCE
BuildSpec: !Sub |
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
- aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
- REPOSITORY_URI=${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryName}
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${!COMMIT_HASH:=latest}
- echo Logging in to Docker Hub...
- echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
- |
cat << EOF > Dockerfile
FROM nginx:latest
EXPOSE 80
EOF
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- 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 images...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
Visibility: PRIVATE
Code language: YAML (yaml)
For information on how to use CodeBuild to push a Docker image to an ECR repository after it has been built, please see the following page.
In the above page, the Dockerfile on the CodeCommit repository was read as source and built in CodePipeline.
This page focuses on how to build with CodeBuild alone.
This time there are no artifacts.
So specify “NO_ARTIFACTS” for the Type property in the Artifacts property.
Also, since the source does not exist, specify “NO_SOURCE” for the Type property in the Source property.
The pre_build phase within the BuildSpec property is the key point.
In this phase, the cat command is used to generate the Dockerfile.
This means that instead of using an external resource such as CodeCommit as the source, the Dockerfile is generated in the CodeBuild environment and built based on this.
When referencing Secrets Manager values in CodeBuild, the EnvironmentVariables property is also a key point.
For details, see the following page, but to set the Secrets Manager value as an environment variable, specify “SECRETS_MANAGER” in the Type property and a string combining the ARN of the secret and the value key in the Value property is a string that combines the ARN of the secret and the key of the value.
The following are the IAM roles for CodeBuild.
Resources:
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
Policies:
- PolicyName: GetSecretValuePolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref Secret
Code language: YAML (yaml)
In addition to the AWS administrative policy AmazonEC2ContainerRegistryPowerUser, you are authorized to reference the values in the Secrets Manager as an inline policy.
Lambda Function
Resources:
CustomResource:
Type: Custom::CustomResource
Properties:
ServiceToken: !GetAtt Function.Arn
Function:
Type: AWS::Lambda::Function
Properties:
Architectures:
- !Ref Architecture
Environment:
Variables:
CODEBUILD_PROJECT: !Ref CodeBuildProject
Code:
ZipFile: |
import boto3
import cfnresponse
import os
codebuild_project = os.environ['CODEBUILD_PROJECT']
CREATE = 'Create'
response_data = {}
client = boto3.client('codebuild')
def lambda_handler(event, context):
try:
if event['RequestType'] == CREATE:
response = client.start_build(
projectName=codebuild_project
)
print(response)
cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
except Exception as e:
print(e)
cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
FunctionName: !Sub "${Prefix}-function"
Handler: !Ref Handler
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)
Define the code to be executed by the Lambda function in inline notation. For more information, please see the following page.
Use the cfnresponse module to implement the function as a Lambda-backed custom resource.
For more information, please see the following page.
The code to be executed is as follows.
- Access os.environ to obtain the environment variables defined in the CloudFormation template.
- Create a client object for CodeBuild in Boto3 and start CodeBuild execution.
IAM roles for functions are as follows
Resources:
FunctionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: GetSecretValuePolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- codebuild:StartBuild
Resource:
- !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProject}"
Code language: YAML (yaml)
In addition to the AWS management policy AWSLambdaBasicExecutionRole, an inline policy gives permission to start CodeBuild.
Architecting
Use CloudFormation to build this environment and check its actual behavior.
Create CloudFormation stacks and check the resources in the stacks
Create CloudFormation stacks.
For information on how to create stacks and check each stack, please see the following page.
After reviewing the resources in each stack, information on the main resources created in this case is as follows
- ECR Repository: fa-129
- Secrets Manager: fa-129
- Lambda function: fa-129-function
- CodeBuild: fa-129
Check each resource from the AWS Management Console.
Check Secrets Manager.
Secret has been successfully created.
Check Lambda functions and execution status.
You can see that the function has been successfully created and executed.
This means that the CloudFormation custom resource automatically executed the Lambda function when the CloudFormation stack was created.
Operation Check
Now that you are ready, check CodeBuild.
Indeed, CodeBuild is running.
This means that the build was started by the execution of the Lambda function associated with the CloudFormation custom resource.
Incidentally, also check CodeBuild’s environment variable settings.
It is indeed set to refer to the Secrets Manager value.
Finally, check the ECR repository.
Certainly the image is being pushed.
Thus, by using CodeBuild and CloudFormation custom resources, we were able to automatically push test images to the ECR repository with images.
Summary
Using CloudFormation custom resources and CodeBuild, we have identified a way to automatically push test images to the ECR repository when creating CloudFormation stacks.