Enable Docker build cache on GitHub Action and deploy to Amazon ECS

things to do

Build a Docker image using GitHub Action, save it to Amazon ECR, and deploy it to Amazon ECS.

--Point --Assuming production operation, Release Make GitHub Action work when you create). --Enable build cache to speed up Docker builds

Preparation

--Create a destination ECR --Create an ECR to deploy to --Create an AWS IAM user for use with GitHub Action (requires ECR and ECS permissions)

Proceed with these as created.

Workflow to create

Create a workflow like the one below and save it to .github/workflows/ in the repository with a suitable name like deploy-to-ecs.yml. By pushing the tag to the main branch, this workflow works, the Docker image is pushed to the ECR and deployed to the ECS. Operate by using [Release] of Github (https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository) I'm assuming that.

.github/workflows/deploy-to-ecs.yml

name: Deploy to ECS 
on:
  push:
    tags:
      - v*

env:
  ECR_REPOSITORY: your-repository-name
  ECS_SERVICE: your-service-name
  ECS_CLUSTER: your-cluster-name

jobs:
  deploy:
    name: Deploy to ECS
    if: github.event.base_ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Configure AWS Credentials #AWS access permission settings
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR #ECR login process
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Set Docker Tag Env #Match Docker Image version to tag
        run: echo "::IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")" >> $GITHUB_ENV

      - name: Build, tag, and push image to Amazon ECR #Docker image build&Push
        env:
          DOCKER_BUILDKIT: 1
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 -f Dockerfile -t $ECR_REPOSITORY .
          docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: Render Amazon ECS task definition for app container #ECS task definition file rendering of app container
        id: render-app-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: .aws/ecs/task-definition.json
          container-name: app
          image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

      - name: Deploy to Amazon ECS service #ECS service deployment
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-app-container.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: false

Commentary

The workflow operation will be explained in detail from the top.

1. Workflow name

name: Deploy to ECS 

I have decided on the name of the workflow. This name will be displayed when you check the workflow in the list from the repository of Github or the Action tab. When creating multiple workflows in a verification or production environment, it is recommended to rename them so that they are easy to distinguish.

2. Workflow operating conditions

on:
  push:
    tags:
      - v*

It works when tags starting with v such as v1.0.0 are pushed, and Release It is assumed that this workflow will work by creating (/ github / administration-a-repository / managing-releases-in-a-repository). Also, this time in jobs so that it will not work if the tag is pushed to another branch

if: github.event.base_ref == 'refs/heads/main'

If it is not the main branch like, it will not work.

To make it work when pushed to a specific branch, remove the if description

on:
  push:
    branches:
      - target-branch

Please describe as. (Change target-branch to the branch you want to deploy.)

Supplement

In order to realize that it works when a tag is pushed to a specific branch by the description method of on: push:

on:
  push:
    tags:
      - v*
     branches:
      - target-branch

If you write like, it will be an or condition (when either the tag or the branch is pushed) instead of the and condition, so specify the target tag with on: push: and target with if The branch of is specified.

3. Assignment to environment variables

env:
  ECR_REPOSITORY: your-repository-name
  ECS_SERVICE: your-service-name
  ECS_CLUSTER: your-cluster-name

I am assigning it to an environment variable to reduce the description of the value that is used many times in job. Please change your-〇〇〇-name to the one in your environment

4. Start Jobs execution

jobs:
  deploy:
    name: Deploy to ECS
    if: github.event.base_ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

Check out to the target branch in the environment of ubuntu-latest and start executing the job. (As I mentioned earlier, I wrote the if line so that it will be executed only in the main branch when the tag is pushed. It is not necessary if the operating condition is changed to push in the branch.)

5. Login to AWS

- name: Configure AWS Credentials #AWS access permission settings
    uses: aws-actions/configure-aws-credentials@v1
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: ap-northeast-1

I am logged in to the AWS account that has created the ECR to push and the ECS to deploy to. Deploy using Github's Secret (https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) IAM access key and secret key AWS_ACCESS_KEY_ID Please register with and AWS_SECRET_ACCESS_KEY.

6. ECR login

 - name: Login to Amazon ECR #ECR login process
    id: login-ecr
    uses: aws-actions/amazon-ecr-login@v1

Log in to the ECR of the AWS account you logged in with in step 5.

7. Docker image tag settings

  - name: Set Docker Tag Env #Match Docker Image version to tag
    run: echo "::IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")" >> $GITHUB_ENV

The tag pushed so that the image tag of Docker has the same value as the tag of git is taken out in a format like v1.0.0 and assigned to the environment variable. If you do not use tags in the workflow operating conditions,

run: echo "::IMAGE_TAG=${{ github.sha }}" >> $GITHUB_ENV

I think it's a good idea to use a commit hash to make the image tag unique.

8. Build Docker image & Push to ECR

  - name: Build, tag, and push image to Amazon ECR #Docker image build&Push
    env:
      DOCKER_BUILDKIT: 1
      ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    run: |
      docker build --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 -f Dockerfile -t $ECR_REPOSITORY .
      docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
      docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

When building a Docker image locally, the build speeds up using the previously built image, but GitHub Action does not retain the previous build result, so it takes time to build each time. Therefore, here, Docker's docker build --cache-from is used to speed up the build. What we are doing is building a docker image, pushing the tag saved in the environment variable IMAGE_TAG, and the value changes dynamically for each workflow operation, so the value at the time of the previous build is used. Since it is difficult to get it, I also push the latest tag and use the latest tag for caching. By doing this, the cache is used while being consistent with the release tag. (DOCKER_BUILDKIT, BUILDKIT_INLINE_CACHE is described because it is necessary to use --cache-from.) Note: If you're using Docker's multi-stage build, you can't cache this method because the Docker image doesn't include the entire build process. In that case, I think you should refer to Articles like this.

9. Create ECS task definition

- name: Render Amazon ECS task definition for app container #ECS task definition file rendering of app container
    id: render-app-container
    uses: aws-actions/amazon-ecs-render-task-definition@v1
    with:
      task-definition: .aws/ecs/task-definition.json
      container-name: app
      image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

To execute ECS, a task definition is required, and it is necessary to describe the repository and tag information of the Docker image to be used there. Therefore, when updating DockerImage, it is also necessary to update the task definition, and that process is being performed.

今回はレポジトリへタスク定義ファイル.aws/ecs/task-definition.jsonを作成してあり、そのファイルを呼び出し、Dockerイメージの情報を更新しています。 Reference example of task-definition.json)

{
  "containerDefinitions": [
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "image": "your-image-name",
      "name": "app"
    }
  ],
  "cpu": "256",
  "executionRoleArn": "your-role-name",
  "family": "your-family",
  "memory": "512",
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "networkMode": "awsvpc"
}

Reading from s3 seems to be possible, so please manage each task definition you like.

10. Deploy to ECS

- name: Deploy to Amazon ECS service #ECS service deployment
    uses: aws-actions/amazon-ecs-deploy-task-definition@v1
    with:
      task-definition: ${{ steps.render-app-container.outputs.task-definition }}
      service: ${{ env.ECS_SERVICE }}
      cluster: ${{ env.ECS_CLUSTER }}
      wait-for-service-stability: false

It is deployed to ECS based on the task definition created in 9. By setting wait-for-service-stability to true, it is possible to wait for the completion of execution until the deployment is completed and notify the completion of deployment, but the execution time of Github Actions will increase. There is a possibility that it will exceed the execution frame. It is also possible to hook the completion of deployment using AWS Lambda and notify it, so it is recommended to do so in an environment with many deployments.

11. End

If the above flow works correctly, the deployment is complete.

Bonus. Execution of task alone

- name: Run Migrate #Run Migration
    env:
      CLUSTER_ARN: your_cluster_arn
      ECS_SUBNER_FIRST: your_subnet_first
      ECS_SUBNER_SECOND: your_subner_second
      ECS_SECURITY_GROUP: your_security_group
    run: |
      aws ecs run-task --launch-type FARGATE --cluster $ECS_CLUSTER --task-definition ${{ steps.put-migrate-task.outputs.render-app-container }} --network-configuration "awsvpcConfiguration={subnets=[$ECS_SUBNER_FIRST, $ECS_SUBNER_SECOND],securityGroups=[$ECS_SECURITY_GROUP],assignPublicIp=ENABLED}" > run-task.log
      TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)
      aws ecs wait tasks-stopped --cluster $CLUSTER_ARN --tasks $TASK_ARN

I wanted to migrate the DB before deploying this time, so I added such a job to the ECS in step 10 before deploying and executed the task alone. By doing this, it is also possible to execute a single task within Github Actions.

Summary

When deploying to ECS, I think there are options such as combining AWS CodeBuild and AWS CodePipeline, but it is also possible to easily deploy using GitHub Action.

Reference article

I have referred to the following article very much.

-Deploy an app from GitHub Actions to Amazon ECS

Recommended Posts

Enable Docker build cache on GitHub Action and deploy to Amazon ECS
Run Rubocop and RSpec on CircleCI and deploy to ECS
Until you run Quarkus and run docker image on Amazon ECS
Deploy Rails on Docker to heroku
Try to build Java8 environment on Amazon Linux2
Test, build, and push your Docker image to GitHub Container Registry using GitHub Actions
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (5)
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (6)
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (3)
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (2)
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (1)
[Part 1] How to deploy Docker containers and static files with CircleCI + ECS + ECR + CloudFront
[Rails AWS Docker] Build an existing Ruby on Rails + MySQL application with Docker and deploy it on AWS (4)
[AWS] What are ECR and ECS? About each difference and how to operate Docker on AWS.
Cache Gradle dependent files to speed up docker build
GitHub Actions/Cache: Cache bundle installed gems on Docker container
Build a Node-RED environment with Docker to move and understand
How to deploy on heroku
Build Metabase with Docker on Lightsail and make it https with nginx
Install Ubuntu20.04 on RaspberryPi 4 and build Kubernetes to run the container
Use Jenkins to build inside Docker and then create a Docker image.