ECS Task DefinitionをActionsで自動更新する

2020.05.25

コンテナのビルドからECS Task Definitionの更新は一貫して行いたい作業であり, CIツールは課題に対する解を提供します.
そしてECSへのデプロイまでに行うべきことはほとんどの場合で同一であります.
なのでCIツールの中で再利用可能な手法が取れればパイプラインの構築を高速にかつ容易にしアプリケーション開発に注力することが出来ます.
GitHub ActionsではAWSが提供しているActionsがありこれを利用することで少ない設定でECSへのデプロイまで実行することが出来ます.
今回はECS Task Definitionの更新までを実際に試してみます.

AWS for GitHub Actions

AWS for GitHub Actions」はAWSリソースへ対する操作をGitHub Actions上から実行する便利なActionsです.
AWSが提供しておりアクセスキーをGitHub Secretsから取得して環境変数に設定したり, Assume Roleをする「configure-aws-credentials」やCloudFormation Stackの作成やChangeSetを作成する「aws-cloudformation-github-deploy」など様々便利なものが定義されています.

その中でも一際数が多いのがECS周辺のActionsでECS Task Definitionの登録や, ECS Serviceに対してECS ControllerとCodeDeployを利用したデプロイを可能にします.
リファレンスを読めば伝わると思うのですが煩雑になりがちなECS Serviceへのデプロイを, CodeDeployを利用したBlue/Greenデプロイをここまで容易にできます.

Register Tsak Definition

ワークフローを定義してもコンテナがないと何もできないのでまずはコンテナとタスク定義を準備します.
今回はざっくり, Node.jsでHTTPサーバを動かします. なのでまずは設定をしてソースコードを書いていきます.

$ yarn init -y
$ yarn add express

次にアプリケーションのコードとDockefileを定義していきます.

srrc/index.js

const express = require('express');

const app = express();
app.get('/', (req, res) => {
  res.send('Hello from ECS on Fargate');
});

app.listen(8080, () => console.log('This app listening on port 8080'));

Dockerfile

FROM node:latest

WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn

COPY . .

EXPOSE 8080
ENTRYPOINT [ "node", "src/index.js" ]

これでアプリケーションの実行に必要な部分が出来上がったので次にタスク定義を書いていきます.
末尾を「ecs-task-def.json」にしてかつ, VSCodeで拡張機能を入れておくと補完が可能なため非常に書きやすくなります. なのでおすすめです.

node-ecs-task-def.json

{
  "family": "sdx_node_web",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecs-exec-role",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [ "FARGATE" ],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "sdx_node_web",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/sdx_node_web",
      "essential": true,
      "memory": 384,
      "memoryReservation": 128,
      "portMappings": [
        {
          "containerPort": 8080
        }
      ]
    }
  ]
}

事前の準備がほとんど完了しました.
ここまでできたので一度CLI経由でTask Definitionを登録してうまく動作するかを確認してみます.
一部出力を省略しましたが問題なさそうですね.

aws ecs --region  us-east-1 register-task-definition \
  --cli-input-json file://node-ecs-task-def.json
  
	{
	    "taskDefinition": {
	        "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/sdx_node_web:1",
	        "containerDefinitions": [
	            {
	                "name": "sdx_node_web",
	                "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/sdx_node_web",
	                "cpu": 0,
	                "memory": 384,
	                "essential": true,
	            }
	        ],
	        "family": "sdx_node_web",
	        "compatibilities": [
	            "EC2",
	            "FARGATE"
	        ],
	        "requiresCompatibilities": [
	            "FARGATE"
	        ],
	        "cpu": "256",
	        "memory": "512"
	    }
	}

Define Workflow

アプリケーションの準備ができたので次にワークフローを定義していきます.
大まかな流れとしては下記のようになります.

  1. クレデンシャルの設定やECR Repositoryにログインする
  2. Docker ImageをビルドしてECR Repositoryにpushする
  3. Task DefinitionのcontainerDefinitions[].imageを書き換える
  4. Task Definitionを更新する

まずは全体のワークフローを眺めてから細かくみていきましょう.

.github/workflows/deploy.yml

name: build and push the image, define new task definition
on:
  push:
    branches: [ master ]
jobs:
  deploy:
    name: push container image and update task definition
    runs-on: ubuntu-latest
    steps:
    # Initial setup
    - name: checkout
      uses: actions/checkout@v2
    - name: configure AWS credentials
      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: us-east-1

    # build and push this image
    - name: login ECR repository
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
    - name: build and push the image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: sdx_node_web
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    - name: logout
      if: always()
      run: docker logout ${{ steps.login-ecr.outputs.registry }}

    # Insert Image URI to Task Definition
    - name: render new task definition
      id: render-container
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: node-ecs-task-def.json
        container-name: sdx_node_web
        image: ${{ steps.login-ecr.outputs.registry }}/sdx_node_web:${{ github.sha }}

    # Update Task Definition
    - name: register new task definition family
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.render-container.outputs.task-definition }}
        cluster: sandcastle

Initial setup

ワークフローで定義したステップの初めの部分であるクレデンシャルの設定部分をみていきます.
実行していることは下記の2つです.

  1. ソースコードをチェックアウトする
  2. GitHub Secretsに登録したIAM アクセスキーを読み込んで環境変数として登録する

GitHub Secretsへの登録はドキュメントをご参照ください.

jobs:
  deploy:
    steps:
    #---
    # Initial setup
    - name: checkout
      uses: actions/checkout@v2
    - name: configure AWS credentials
      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: us-east-1

Build and push this image

Docker ImageをビルドしてECR リポジトリにpushします.
ECR リポジトリにログインするステップの出力を後ほど利用するため, 「login-ecr」というIDを付与するのを忘れないでください.
そのあとは, Gitのcommit hashを利用してタグ付けを行い, Docker Imageをpush, 最後にECRリポジトリからログアウトします.

jobs:
  deploy:
    steps:
    #---
    # build and push this image
    - name: login ECR repository
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
    - name: build and push the image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: sdx_node_web
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    - name: logout
      if: always()
      run: docker logout ${{ steps.login-ecr.outputs.registry }}

Insert image URI to Task Definition

Task DefinitionのcontainerDefinitions[].imageを最新のもの, つまり先ほどpushしたDocker ImageのURIに書き換えます.
自前で定義すると大変な実装になりそうですがAWSがamazon-ecs-render-task-definitionという, Task DefinitionのImage URIを書き換えるActionを提供しています.
なのでこれを使って簡単に新しいTask Definitionを作り出せます. また後のステップで出力を利用するのでID付与をお忘れなく.

jobs:
  deploy:
    steps:
    #---
    # Insert Image URI to Task Definition
    - name: render new task definition
      id: render-container
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: node-ecs-task-def.json
        container-name: sdx_node_web
        image: ${{ steps.login-ecr.outputs.registry }}/sdx_node_web:${{ github.sha }}

Update Task Definition

長い道のりを終えて最後にTask Definitionを更新します.
今回は検証が目的なのでTask Definitionの更新にとどめていますが本来であればamazon-ecs-deploy-task-definitionは物凄く強力なActionです.
どのくらい強力かというと下記のようなことが可能です. つまりECSに対するデプロイ手法はかなり抑えています.

  • Task Definitionの更新
  • ECS ServiceのRolling Update
  • ECS ServiceのCodeDeployを利用したBlue/Green デプロイ

設定量自体も今までより少なく, かついままでの出力を利用するので非常にシンプルです.

jobs:
  deploy:
    steps:
    #---
    # Update Task Definition
    - name: register new task definition family
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.render-container.outputs.task-definition }}
        cluster: sandcastle

Release it

ここまで準備ができたのでコードをCommitしてGitHubを確認してみましょう.
ちょっとだけ味気ないですが実際のActionsの画面はこのようになります.

img

次にTask Definitionを確認してみましょう.
1度CLIから作成しているのでリビジョンは2になります.

aws --region us-east-1 ecs describe-task-definition \
  --task-definition arn:aws:ecs:us-east-1:123456789012:task-definition/sdx_node_web:2
		{
		    "taskDefinition": {
		        "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/sdx_node_web:2",
		        "containerDefinitions": [
		            {
		                "name": "sdx_node_web",
		                "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/sdx_node_web:6320743afa95ea0f6f5df9cb3d2c3fb0e872b150",
		                "cpu": 0,
		                "memory": 384,
		                "memoryReservation": 128,
		                "portMappings": [
		                    {
		                        "containerPort": 8080,
		                        "hostPort": 8080,
		                        "protocol": "tcp"
		                    }
		                ],
		                "essential": true,
		            }
		        ],
		        "family": "sdx_node_web",
		        "cpu": "256",
		        "memory": "512"
		    }
		}

リビジョンが更新されていてかつ, URIの末尾も変わっていますね.

To close

CIツールを利用した自動化は今の時代必須に近い項目であり, セットアップを簡単にできることは開発者にとって大きな恩恵をもたらします.
GitHub ActionsはGitHubと統合されていることもさることながらコミュニティによりActionsの提供で設定を簡単にできることも大きな恩恵だと思います.
この記事が誰かのお役に立てたら幸いです.

Reference