Automatically push test images to ECR using CFN custom resources and CodeBuild

Automatically push test image to ECR using CFN custom resources and CodeBuild.

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

Diagram of automatically push test image to ECR using CFN custom resources and CodeBuild.

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.

https://awstut.com/en/2023/04/15/use-secrets-manager-to-generate-random-password-en

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.

https://awstut.com/en/2022/08/14/use-codepipeline-to-trigger-codecommit-pushes-to-push-images-to-ecr-en

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.

https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.secrets-manager

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.

https://awstut.com/en/2022/02/02/3-parterns-to-create-lambda-with-cloudformation-s3-inline-container

Use the cfnresponse module to implement the function as a Lambda-backed custom resource.
For more information, please see the following page.

https://awstut.com/en/2022/05/04/introduction-to-cloudformation-custom-resources-en

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.

https://awstut.com/en/2021/12/11/cloudformations-nested-stack

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.

Detail of Secrets Manager 1.

Secret has been successfully created.

Check Lambda functions and execution status.

Detail of Lambda 1.
Detail of Lambda 2.

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.

Detail of CodeBuild 1.

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.

Detail of CodeBuild 2.

It is indeed set to refer to the Secrets Manager value.

Finally, check the ECR repository.

Detail of ECR 1.

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.