Simplify ECR image building and publishing with CodePipeline V2

Codepipeline + ECR

AWS recently announced two new actions for CodePipeline: ECRBuildAndPublish and InspectorScan, let’s see how the first one can simplify the process of building and publishing Docker images to Amazon ECR.

🚨 Need a full example? I published a complete Terraform template here: daniele-salvagni/codepipeline-ecr-build-and-publish-tf

Before the update

Before this update, the process of building and publishing a Docker image to Amazon ECR involved using CodeBuild with a custom buildspec.yml which looked something like this:

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login
        --username AWS --password-stdin
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

I am sure you also had to copy-paste this boilerplate more than once from the AWS documentation. Moreover, you had to write infrastructure code to also create the CodeBuild resources.

The new ECRBuildAndPublish action

With the introduction of the ECRBuildAndPublish action for CodePipeline (v2), it is no longer necessary to write a buildspec, as everything is managed by AWS which still uses CodeBuild under the hood.

Here is an example within a CloudFormation template for building the image, and publishing it with the “latest” and commit ID tags to an ECR repository (the relevant part is the Build stage):

Resources:
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      # ...
      Stages:
        # Get the source code from a repository
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeStarSourceConnection
                Version: '1'
              Configuration:
                ConnectionArn: !Sub MyCodeStarConnectionArn
                FullRepositoryId: !Sub MyFullRepositoryId
                BranchName: !Sub MyBranchName
              Namespace: SourceVars
              OutputArtifacts:
                - Name: source_output
              RunOrder: 1

        # Build and publish the Docker image to ECR
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: ECRBuildAndPublish
                Version: '1'
              Configuration:
                ECRRepositoryName: !Sub MyECRRepositoryName
                DockerFilePath: ./
                ImageTags: '#{SourceVars.CommitId},latest'
              Namespace: BuildVars
              InputArtifacts:
                - Name: source_output
              RunOrder: 1

Or, the equivalent in Terraform:

resource "aws_codepipeline" "codepipeline" {
  name          = "${var.resource_prefix}-pipeline"
  pipeline_type = "V2"

  role_arn = aws_iam_role.codepipeline_role.arn

  artifact_store {
    location = aws_s3_bucket.artifact_store.bucket
    type     = "S3"
  }

  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      namespace        = "SourceVars"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = 1
      output_artifacts = ["source_output"]
      configuration = {
        ConnectionArn    = var.source_codestar_arn
        FullRepositoryId = var.source_full_repo_id
        BranchName       = var.source_branch_name
      }
    }
  }

  stage {
    name = "Build"
    action {
      name            = "Build"
      category        = "Build"
      namespace       = "BuildVars"
      owner           = "AWS"
      provider        = "ECRBuildAndPublish"
      version         = 1
      input_artifacts = ["source_output"]

      configuration = {
        ECRRepositoryName = var.ecr_repo_name
        DockerFilePath    = var.dockerfile_path
        ImageTags         = "#{SourceVars.CommitId},latest"
      }
    }
  }
}

Also note how, in both examples, it is possible to use Namespaces to pass action variables between stages with the #{...} syntax (e.g., the commit ID).