Github ActionからECRにpushしてECS Fargateにデプロイする

バックエンドAPIをGithub ActionからECS Fargateにデプロイするサンプル設定をまとめてみました。

設定自体はNest.js(Node.js)やRails, Laravelなど他のWebフレームワークでも同様だと思います。

# AWS環境の構成

AWSの環境はおおよそこんな感じの構成です。

- IAM user: prod-backend-api-deploy
  - 許可するポリシー
    - ECR Push
    - ECS Deploy
- ECR: backend-api
- ECS: prod-backend-api
  - VPC: prod
    - subnet: internal
    - security group: 80番ポートへのアクセスを許可
  - execution role: prod-backend-api-ecs-execution
    - 許可するポリシー
      - ECR Pull
      - Log Write
  - task role: prod-backend-api-ecs-task
    - 許可するポリシー(SecretManagerの認証情報を使ってRDSに接続する場合)
      - Secret Manager Read
      - RDS Data Full Access
  - ALB: prod-alb
    - Target Group: prod-backend-api
  - CloudWatch log group: /ecs/prod-backend-api

# Github Actionの作成

- 事前準備
  - AWSのIAMユーザ(prod-backend-api-deploy)のアクセスキーを発行して、Githubリポジトリのproduction環境の環境変数に認証情報を設定しておきます。

```yaml:.github/workflows/deploy-prod.yml
name: Deploy to Production

on:
  push:
    branches:
      - main
  workflow_dispatch:

env:
  AWS_REGION: ap-northeast-1
  AWS_ACCOUNT_ID: xxx
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  ECR_REPOSITORY: backend-api
  ECS_CLUSTER_NAME: prod-backend-api
  ECS_SERVICE_NAME: backend-api
  IMAGE_TAG: ${{ github.sha }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
      - name: Checkout code
        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: ${{ env.AWS_REGION }}

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

      - name: Build and Push Docker image to ECR
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build . -f ./docker/api/Dockerfile --target production --tag ${{ env.REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
          docker push ${{ env.REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        with:
          task-definition: .aws/task-definition.prod.json
          container-name: backend-api
          image: ${{ env.REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

      - name: Deploy ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          cluster: ${{ env.ECS_CLUSTER_NAME }}
          service: ${{ env.ECS_SERVICE_NAME }}
          wait-for-service-stability: true
```

Github Actionで参照するタスク定義の内容はこんな感じです。

```json:.aws/task-definision.prod.json
{
  "requiresCompatibilities": [ "FARGATE" ],
  "family": "prod-backend-api",
  "containerDefinitions": [
    {
      "name": "prod-backend-api",
      "environment": [],
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
            "awslogs-group": "/ecs/prod-backend-api",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "awslogs-prod-api"
        }
      }
    }
  ],
  "volumes": [],
  "networkMode": "awsvpc",
  "memory": "512",
  "cpu": "256",
  "runtimePlatform": {
    "cpuArchitecture": "X86_64",
    "operatingSystemFamily": "LINUX"
  },
  "taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/prod-backend-api-ecs-task",
  "executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/prod-backend-api-ecs-execution"
}
```

テスト用のDockerfileはこんな感じのものを用意すればOKです。

```Dockerfile:./docker/api/Dockerfile
FROM httpd:2.4 as production

RUN mkdir /usr/local/apache2/conf/include/
RUN echo 'Include conf/include/*.conf ' >> /usr/local/apache2/conf/httpd.conf
RUN echo 'ServerName localhost:80' > /usr/local/apache2/conf/include/test.conf
```