正在閱讀CI/CD pipeline with Github Action and Terraform for Amazon ECS - Build and Push to ECR

CI/CD pipeline with Github Action and Terraform for Amazon ECS - Build and Push to ECR

Ivan Wong

Ivan Wong

2022-11-24

Card image

Recently, I got a chance to set up a CI/CD pipeline to automatically build a container image and deploy to Amazon ECS from a GitHub repo. There are actually quite a lot of online articles providing comprehensive tutorial on how to set up a GitHub Action for such a CI/CD to Amazon ECS. But, since I am an active IaC (Infrastructure as Code) user, I provision all the cloud resources with Terraform as much as I can. Most of the articles did not cover the part of how to leverage Terraform under this situation, so I decide to write one. I will separate the article into two halfs. In the first half, we only finish the CI part, that is building the container image and upload to Amazon ECR. We will complete the CD part in the next article.

Assumption and Prerequisite

To make the article concise, I assume the reader has background knowledge on Terraform and already set up the correct credentials to provision AWS resources through Terraform. Also, I assume that you have set up an ECS cluster that working normally. In this CI/CD pipeline, we only build one image and deploy one ECS Fargate task.

Define local variables

At the beginning, let us define some local variables so that we can reuse them in the later parts:

1locals { 2 app_fullname="example-app" 3 container = { 4 name = "${local.app_name}" 5 http_port = 80 6 } 7 fargate_execution_role_name = "${local.app_fullname}-fargate-execution-role" 8 fargate_execution_role_policy_name = "${local.fargate_execution_role_name}-policy" 9 fargate_task_role_name = "${local.app_fullname}-fargate-task-role" 10 fargate_task_role_policy_name = "${local.fargate_task_role_name}-policy" 11 ecr_publisher_name = "ecr-publisher" 12 ecr_publisher_policy_name = "${local.ecr_publisher_name}-policy" 13}

Provision an ECR repository and IAM role for GitHub action

In this project, we use AWS ECR to store the container image built by the GitHub action. We provision an ECR repository for the storing image.

1resource "aws_ecr_repository" "backend_repository" { 2 name = "${local.app_fullname}" 3 image_tag_mutability = "MUTABLE" 4 tags = local.default_tags 5}

And, we also need an IAM role for a GitHub action to access the ECR repo:

1resource "aws_iam_user" "publisher" { 2 name = local.ecr_publisher_name 3 path = "/serviceaccounts/" 4 5 tags = local.default_tags 6} 7 8resource "aws_iam_user_policy" "publisher" { 9 name = local.ecr_publisher_policy_name 10 user = aws_iam_user.publisher.name 11 12 policy = <<EOF 13{ 14 "Version": "2012-10-17", 15 "Statement": [ 16 { 17 "Action": [ 18 "iam:PassRole", 19 "iam:GetRole", 20 "ecs:DescribeTaskDefinition", 21 "ecs:DescribeServices", 22 "ecs:UpdateService", 23 "ecs:RegisterTaskDefinition", 24 "ecr:CompleteLayerUpload", 25 "ecr:DescribeRepositories", 26 "ecr:ListImages", 27 "ecr:DescribeImages", 28 "ecr:GetAuthorizationToken", 29 "ecr:GetDownloadUrlForLayer", 30 "ecr:GetLifecyclePolicy", 31 "ecr:InitiateLayerUpload", 32 "ecr:PutImage", 33 "ecr:UploadLayerPart" 34 ], 35 "Effect": "Allow", 36 "Resource": "*" 37 } 38 ] 39} 40EOF 41} 42 43resource "aws_iam_access_key" "publisher" { 44 user = aws_iam_user.publisher.name 45}

We need to define some outputs, such as the access key and secret key of the IAM, we will insert that to the GitHub action's secret later:

1output "ecr_repository_name" { 2 value = aws_ecr_repository.backend_repository.name 3 description = "The ECR repository name" 4} 5 6output "publisher_access_key" { 7 value = aws_iam_access_key.publisher.id 8 description = "AWS_ACCESS_KEY to publish to ECR" 9} 10 11output "publisher_secret_key" { 12 value = aws_iam_access_key.publisher.secret 13 description = "AWS_SECRET_ACCESS_KEY to upload to the ECR" 14 sensitive = true 15}

Now, we can apply the Terraform script and print the outputs after the provision:

1# Remember to check whether the deployments are correct before going ahead 2$ terraform apply 3# Then print the variables in JSON format, including secrets 4$ terraform output -json 5 6These outputs are important and we will make use of them in the GitHub Action.

Write the GitHub action

After provisioning the resources, we can start writing our GitHub action. There are few tasks that we need to accomplish in the action:

  • Test our code
  • Build the image
  • Push to the Amazon ECR

I assume the readers are using the basic plan of GitHub, in which the GitHub action doesn't support environments (but it allows environment variables written in script or inherited from other actions). Only secret is supported.

We begin with defining the condition of running the action and some environments variables. We hope that the action runs when there is a new push on the staging branch. To prevent unnecessary runs, we should ignore changes that only occuried on files such as .dockerignore, .gitignore or markdown.

1name: Example app 2 3on: 4 workflow_dispatch: 5 push: 6 branches: 7 - staging 8 paths-ignore: 9 - .dockerignore 10 - .gitignore 11 - '**/*.md' 12 13env: 14 CI_VERSION: snapshot.${{github.sha}} 15 PROJECT_NAME: <your project name> 16 ENV_NAME: staging 17 APP_NAME: example 18 ECR_REPO_NAME: <ECR repo name> 19 MAINTAINER: "Ivan Wong"

Remember, you need to put the name of the ECR repo that you just provisioned here.

Then, we can start defining our job details. We have two jobs in the action: CI and CD. CI is responsible for linting and testing the code that you push. You should modify run arguments to the commands that suits your project.

1jobs: 2 ci: 3 name: CI 4 runs-on: ubuntu-latest 5 steps: 6 - uses: actions/checkout@v2 7 8 - name: "Lint code" 9 run: echo "Linting repository" 10 11 - name: "Run unit tests" 12 run: echo "Running unit tests"

The CD job responsible for building the image and pushing to the ECR repo. We use some predefined actions:

  • [FranzDiebold/github-env-vars-action@v2](https://github.com/FranzDiebold/github-env-vars-action): get some GitHub specific environments variables, such as env.CI_ACTION_REF_NAME_SLUG and env.CI_SHA_SHORT
  • actions/checkout@v2: clone and check against your repo
  • aws-actions/configure-aws-credentials@v1: configure the AWS credentials with the access key inserted in GitHub action secrets. We will talk about it in the next session.
1 cd: 2 name: CD 3 runs-on: ubuntu-latest 4 needs: 5 - ci 6 7 steps: 8 # Expose useful environment variables to the action 9 - uses: FranzDiebold/github-env-vars-action@v2 10 11 # Clone and checkout 12 - uses: actions/checkout@v2 13 14 - name: Configure AWS credentials 15 uses: aws-actions/configure-aws-credentials@v1 16 with: 17 aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 18 aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 19 aws-region: ${{ secrets.AWS_REGION }} 20 21 - name: Login to Amazon ECR 22 id: login-ecr 23 uses: aws-actions/amazon-ecr-login@v1 24 25 # Start building 26 - name: Set up Docker Buildx 27 uses: docker/setup-buildx-action@v1

We also need to define steps for building the container images. In the below script, we assume your Dockerfile is located at the root of the repo. If not, you will need to modify the context and the file.

In the tags session, we define two tags for the image, both have the same prefix to guide the GitHub action to push to the ECR repo. The suffixes are different, one is tagged with ${{ env.CI_ACTION_REF_NAME_SLUG }}.${{ env.CI_SHA_SHORT }}, i.e. <branch name>.<short hash of the commit>. The second one is tagged with latest.

So whether the GitHub action build a new image, the newest image will always being tagged with latest and <branch name>.<short hash of the commit>, while the old latest image will have only <branch name>.<short hash of the commit>.

1 - name: Cache Docker layers 2 uses: actions/cache@v2 3 with: 4 path: /tmp/.buildx-cache 5 key: ${{ runner.os }}-buildx-${{ github.sha }} 6 restore-keys: | 7 ${{ runner.os }}-buildx- 8 9 - name: Build docker 10 uses: docker/build-push-action@v2 11 with: 12 context: . 13 file: Dockerfile 14 push: true 15 build-args: | 16 VERSION=latest 17 tags: | 18 ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPO_NAME }}:${{ env.CI_ACTION_REF_NAME_SLUG }}.${{ env.CI_SHA_SHORT }} 19 ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPO_NAME }}:latest 20 labels: 21 project=${{ env.PROJECT_NAME }} 22 name=${{ env.APP_NAME }} 23 environment=${{ env.ENV_NAME }} 24 maintainer=${{ env.MAINTAINER }} 25 repository=${{ github.repository }} 26 gh_job=${{ github.job }} 27 cache-from: type=local,src=/tmp/.buildx-cache 28 cache-to: type=local,dest=/tmp/.buildx-cache

Cool! So let's push the Github action YAML to your repo, you can place it in the directory .github/workflows, e.g. .github/workflows/staging.yaml.

AWS access key

Don't forget to upload the access key of your IAM role. Open your repo, Settings -> Security -> Secrets -> Actions.
Create the secrets AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY that you obtained from Terrafrom output. For AWS_REGION, type the region that you have been using, e.g. ap-southeast-1

At here, you can put the secrets used by your GitHub action.

At here, you can put the secrets used by your GitHub action.

Let's trigger the GitHub action

So, let's push a commit to your staging branch. The GitHub action will run and push the image to your ECR repo.

The Github Action run successfully and the commit hash is 088977e as shown.

The Github Action run successfully and the commit hash is 088977e as shown.

Login your AWS account and enter your ECR repo, you will see something similar:

Open the ECR repo in your AWS account, the latest image is tagged with 088977e as well.

Open the ECR repo in your AWS account, the latest image is tagged with 088977e as well.


See, the latest image is tagged with latest, while other old images are tagged with <branch name>.<short hash of the commit> only.

What's next

Right now, we only complete the part that test, build and push the image to Amazon ECR. We also need to automatically deploy the new image to our ECS cluster, which will be complete in the second half of this article. Stay tuned! I will also publish a GitHub repo of all the codes that we used in here later after finishing the whole series.

其他關鍵字: CI/CD pipeline,GitHub Action,Terraform,ECR

此文章撰寫自
Ivan Wong

Ivan Wong

@spectre

I am a software engineer, familiar with programming languages such as Go, Typescript and C++. Right now has been working as a DevOps engineer, so I'm also studying some DevOps things, mostly related to AWS and Kubernetes.

我是一名軟件工程師,比較熟悉 Go、Typescript 及 C++。目前的職位是 DevOps 工程師,所以也在學習一些 DevOps 知識,請大家多多指教!