GitHub Actionsを使ってECS Fargate(AWS)へ自動デプロイした

# はじめに
GitHub Actions(以下GHA)を使ってECS Fargate(AWS)へ自動デプロイしたので手順を記します。

# 対象
こちらのDjangoアプリをデプロイします。

https://qiita.com/So1111/items/a9b14141833db9dfb502

今回はmainブランチへのマージをトリガーにECRへのコンテナイメージのプッシュとECSのサービスの更新が確認できれば良いため、リソースは最小構成にします。

![Architecture3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3088985/5e522f01-488f-487d-811b-f3568353a449.jpeg)

# 手順
## 1. GHAからAWSへの連携
連携する際のクレデンシャルとしてSecrets又はOpenID Connectがありますが、セキュリティ面からOpenID Connectを選択します。

||概要|メリット|デメリット|
|---|---|---|---|
|Secrets|GitHub上に保存したワードでAWSにアクセスする|・設定が容易|・長期保存のため漏洩した際の影響が大きい<br>・ローテーション作業が必要|
|OpenID Connect|GitHubからトークンを取得し、AWSとの間でトークンと一時クレデンシャルを交換し、一時クレデンシャルでAWSにアクセスする|・一時保存のため漏洩した際の影響が小さい<br>・ローテーション作業が不要|・設定が複雑（ただし初回のみ）|

### OpenID Connect Providerの作成
トークンと一時クレデンシャルを交換するために必要な設定です。
```:ターミナル
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 1234567890123456789012345678901234567890
```
引用：[example-github-cicd](https://github.com/tmknom/example-github-cicd/blob/main/command/11/README.md)

### IAMポリシーの作成・ロールへのアタッチ
どこから(GitHubレポジトリ)どこに(AWS)アクセスできるかを制御します。
```:ポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ID}:oidc-provider/${PROVIDER_URL}"
      },
      "Condition": {
        "StringLike": {
          "${PROVIDER_URL}:sub": "repo:${GITHUB_REPOSITORY}:*"
        }
      }
    }
  ]
}
```
引用：[example-github-cicd](https://github.com/tmknom/example-github-cicd/blob/main/command/11/README.md)

### ワークフローの作成
下記のdeploy.ymlに認証アクションを組み込み(Get tmp credentials)、一時クレデンシャルを取得します。その際に必要なパラメータ(AWSアカウントIDなど)についてはSecrets(GitHub上で設定)に登録して使用します。こちらはクレデンシャルではないため、万が一漏れても影響は限定的です。


## 2. GHAからAWSへコンテナデプロイ
### IAMポリシーの作成・ロールへのアタッチ
ECR, ECSへアクセスするため、ECRへのイメージのプッシュやECSのサービス更新権限等を加味したIAMポリシーを作成し、ロールへアタッチします。
```:ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetDownloadUrlForLayer",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart",
                "ecs:DescribeTaskDefinition",
                "ecs:RegisterTaskDefinition",
                "ecs:UpdateService",
                "ecs:DescribeServices"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Condition": {
                "StringLike": {
                    "iam:PassedToService": "ecs-tasks.amazonaws.com"
                }
            }
        }
    ]
}
```
引用：[example-github-cicd](https://github.com/tmknom/example-github-cicd/blob/main/command/12/README.md)

### ワークフローの作成
```.github/workflows/deploy.yml
name: Deploy
on:
  pull_request:
    branches:
      - main
    types: [closed]
env: # AWSの認証アクションへ指定する入力パラメータを環境変数として定義
  ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
  SESSION_NAME: gh-oidc-${{ github.run_id }}-${{ github.run_attempt }}
jobs:
  deploy:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write # OIDCトークンの取得を許可
    env:
      ECS_CLUSTER_NAME: django-tw-cluster
      ECS_SERVICE_NAME: django-tw-service
    steps:
      - uses: actions/checkout@v4

      - name: Get tmp credentials # AWSの認証アクション
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_ARN }}
          role-session-name: ${{ env.SESSION_NAME }}
          aws-region: ap-northeast-1
      
      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: web push to ECR
        id: build-web
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: django-tw/web
        run: |
          docker build -t $ECR_REPOSITORY -f Dockerfile .
          docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: db push to ECR
        id: build-db
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: django-tw/db
        run: |
          docker build -t $ECR_REPOSITORY -f Dockerfile.db .
          docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: nginx push to ECR
        id: build-nginx
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: django-tw/nginx
        run: |
          docker build -t $ECR_REPOSITORY -f Dockerfile.nginx .
          docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
      
      - name: Deploy to ECS
        run: |
          aws ecs update-service --service $ECS_SERVICE_NAME --cluster $ECS_CLUSTER_NAME --force-new-deployment
```

#### 解説
- mainブランチにマージされたらECS Fargateへデプロイされるようにするため、イベント(on)とif文を工夫します

https://qiita.com/okazy/items/7ab46f2c20ec341a2836

- AWSの一時クレデンシャルを取得してからデプロイする必要があるため、Get tmp credentialsで認証アクションを用いて一時クレデンシャルを取得します(コメント部分)

<div style="margin-bottom: 8px;"></div>

- メインの処理はビルドとデプロイです。ビルドはLogin to ECR ＋ xxx push to ECR(xxxはコンテナ名)、デプロイはDeploy to ECSで記述しています。これらのビルドとデプロイは各々アクションに切り出してコンポーネント化できそうですが、今回はわかりやすくシェルコマンドで実装しました


# おわりに
無事デプロイできました。次はTerraformでインフラを定義します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3088985/76402f86-d559-41c9-ae3f-445e7279423f.png)


# 参考文献
全体的に以下を参考とさせていただきました。

https://gihyo.jp/book/2024/978-4-297-14173-8

https://www.udemy.com/course/ecs-githubactions/?couponCode=ST17MT31325G2