GitHub ActionsでECSに自動デプロイする [ 備忘録 ]

## はじめに
クラウドネイティブな環境において、継続的インテグレーション・継続的デリバリー（CI/CD）パイプラインの構築は、開発効率の向上とリリースサイクルの高速化に不可欠です。この記事では、GitHub Actionsを使用してAWS ECS（Elastic Container Service）にアプリケーションを自動デプロイする方法を解説します。
特に注目すべき点は、GitHub ActionsとAWSの連携にOpenID Connect（OIDC）を使用することで、長期的なアクセスキーを保存せずに安全な認証を実現していることです。これにより、セキュリティリスクを大幅に軽減しながら、効率的なデプロイパイプラインを構築できます。

## 前提条件
こちらを作成している。

https://qiita.com/oyne2561/items/7cafb1eb97bf6b37aa92

## 今回使用したリポジトリ
https://github.com/oyne2561/ecs-github-actions-learning

## GitHub Actionsワークフローの定義
cicd.yml
```yaml 
name: API Deploy Pipeline
on:
  push:
    paths:
      # apiフォルダ内とこのyamlファイルに変更があった場合のみcicdを走らせるという条件
      # YAML ファイルのパス指定はリポジトリのルートディレクトリ基準で解釈される
      - '.github/workflows/**'
      - 'api/**'

# 以下のjobsで使える環境変数を定義
env:
  AWS_REGION: ap-northeast-1
  ECS_CLUSTER: my-app-cluster
  ECS_SERVICE: my-app-api-service
  ECS_REPOSITORY: my-app-api
  ECS_TASK_DEFINITION_API: .aws/task-def-api.json

# Open ID Connectという手法を使って認証を行う際に必要な記述
permissions:
  id-token: write
  contents: read

jobs:
# Test/Build
  test-and-build:
    runs-on: ubuntu-latest
    defaults:
      run:
        # apiディレクトリをワーキングディレクトリに指定
        working-directory: api
    steps:
      # Checkout code コードをダウンロードする、コードをワーキングディレクトリに持ってくるみたいなこと
      # usesはgithub actionsのテンプレートを参照する時に使用する。第三者が作った便利なものを使う
      # https://github.com/marketplace/actions/checkout
      - uses: actions/checkout@v4

      - name: Run Tests and Build an Image
        run: docker image build -t temp_api_image:latest .


      - name: Configure AWS credentials
        # https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ env.AWS_REGION }}
          # 秘匿性の高い情報を扱いたいときはsecretsという別の領域を使う。githubのsettingで定義する。
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}

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

      - name: Push the image to Amazon ECR
        # このステップだけで有効な環境変数
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        # 仮に命名したTagの名前を書き換える
        # `github.sha`はコミットのハッシュを取得できる 例: 52c94cds
        run: |
          docker image tag temp_api_image:latest $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
          docker image push $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
# Deploy
```

## Open ID Connectに関するAWS側の設定
### IAM IDプロバイダ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/39e6de48-05d6-4cbe-9bfd-a823a8b65b3e.png)

参考資料
https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws

`https://token.actions.githubusercontent.com`
`sts.amazonaws.com`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/d4ec86d4-2944-4eb0-85e7-ad3e6a492c73.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/4be0409c-b5da-4ea1-a14a-3e38e0206476.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/78e1ce5f-2269-4dec-b03d-0a1c6727f1ec.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/1865b522-9e61-4d6c-bd64-ddcdbf1000bf.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/12c69170-56e5-4bef-ade9-4b7ecf1f6218.png)

`GitHubActionsEcsLearning`
`Role for Github Actions ecs-github-actions-learning repository.`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/7a0fa55e-212e-473c-a51f-0c936f8b6943.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/a15c145e-a225-46df-92f3-53649b5b2911.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/8daba9b6-e1f1-44fe-a887-80779c1f1b66.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/82caf9bc-64cd-432b-afba-abcbd776a196.png)

スクショ内のコードは以下。
```
{
	"Version": "2012-10-17",
	"Statement": [
		{
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecs:UpdateService",
                "ecs:RegisterTaskDefinition",
                "ecs:ListTaskDefinitions",
                "ecs:DescribeServices"
            ],
            "Resource": "*"
        }
	]
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/d2428b6c-d334-46e3-af40-3b3b76958a40.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/a35c9388-885e-450e-9307-055c59e8477e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/055a64ea-e825-4d00-8b17-997a3b8bd666.png)


## `secrets.AWS_ROLE_TO_ASSUME` の設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/95912141-5a03-4757-ae21-15985da284b8.png)

`AWS_ROLE_TO_ASSUME`
作成したIAMロールである`GitHubActionsEcsLearning`のARNをコピーして、`AWS_ROLE_TO_ASSUME`のSecretに貼り付ける。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/a79b8ee5-264f-4591-8383-2709dadf5207.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/c3e7c265-91f5-4899-85f1-91dd618cff63.png)

**cicd.ymlに追記**
```yaml 
name: API Deploy Pipeline
on:
  push:
    paths:
      # apiフォルダ内とこのyamlファイルに変更があった場合のみcicdを走らせるという条件
      # YAML ファイルのパス指定はリポジトリのルートディレクトリ基準で解釈される
      - '.github/workflows/**'
      - 'api/**'

# 以下のjobsで使える環境変数を定義
env:
  AWS_REGION: ap-northeast-1
  ECS_CLUSTER: my-app-cluster
  ECS_SERVICE: my-app-api-service
  ECR_REPOSITORY: my-app-api
  ECS_TASK_DEFINITION_API: .aws/task-def-api.json

# Open ID Connectという手法を使って認証を行う際に必要な記述
permissions:
  id-token: write
  contents: read

jobs:
# Test/Build
  test-and-build:
    runs-on: ubuntu-latest
    defaults:
      run:
        # apiディレクトリをワーキングディレクトリに指定
        working-directory: api
    steps:
      # Checkout code コードをダウンロードする、コードをワーキングディレクトリに持ってくるみたいなこと
      # usesはgithub actionsのテンプレートを参照する時に使用する。第三者が作った便利なものを使う
      # https://github.com/marketplace/actions/checkout
      - uses: actions/checkout@v4

      - name: Run Tests and Build an Image
        run: docker image build -t temp_api_image:latest .


      - name: Configure AWS credentials
        # https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ env.AWS_REGION }}
          # 秘匿性の高い情報を扱いたいときはsecretsという別の領域を使う。githubのsettingで定義する。
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}

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

      - name: Push the image to Amazon ECR
        # このステップだけで有効な環境変数
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        # 仮に命名したTagの名前を書き換える
        # `github.sha`はコミットのハッシュを取得できる 例: 52c94cds
        run: |
          docker image tag temp_api_image:latest $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
          docker image push $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
# Deploy
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/67ba96a3-a1ef-420a-bef4-06a3e724c8f6.png)

ECRにプッシュできている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/00f3704b-8dc1-426b-9222-e731702610dd.png)


定義したstepが通っているのを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/91d0058f-ab05-43ba-8076-f7f2ed8bdf81.png)

## ECSへのデプロイ
ECSのアプリをアップデートするとは、Task定義(JSON)を更新し、デプロイするということ。リビジョン番号を更新しデプロイし直す。具体的にはimageを変えるだけ。

`cicd.ymlの更新`
```
name: API Deploy Pipeline
on:
  push:
    paths:
      # apiフォルダ内とこのyamlファイルに変更があった場合のみcicdを走らせるという条件
      # YAML ファイルのパス指定はリポジトリのルートディレクトリ基準で解釈される。
      - '.github/workflows/**'
      - 'api/**'

# 以下のjobsで使える環境変数を定義
env:
  AWS_REGION: ap-northeast-1
  ECS_CLUSTER: my-app-cluster
  ECS_SERVICE: my-app-api-service
  ECR_REPOSITORY: my-app-api
  ECS_TASK_DEFINITION_API: .aws/task-def-api.json

# Open ID Connectという手法を使って認証を行う際に必要な記述
permissions:
  id-token: write
  contents: read

jobs:
  # Test/Build
  test-and-build:
    runs-on: ubuntu-latest
    defaults:
      run:
        # apiディレクトリをワーキングディレクトリに指定
        working-directory: api
    steps:
      # `actions/checkout@v4`の役割: リポジトリのコードをワークフローの実行環境にダウンロード（チェックアウト）する
      # usesはgithub actionsのテンプレートを参照する時に使用する。第三者が作った便利なものを使う
      # https://github.com/marketplace/actions/checkout
      - uses: actions/checkout@v4

      - name: Run Tests and Build an Image
        run: docker image build -t temp_api_image:latest .


      - name: Configure AWS credentials
        # https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ env.AWS_REGION }}
          # 秘匿性の高い情報を扱いたいときはsecretsという別の領域を使う。githubのsettingで定義する。
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}

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

      - name: Push the image to Amazon ECR
        # このステップだけで有効な環境変数
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        # 仮に命名したTagの名前を書き換える
        # `github.sha`はコミットのハッシュを取得できる 例: 52c94cds
        run: |
          docker image tag temp_api_image:latest $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
          docker image push $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}
          echo $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }} > api-image-uri.txt

      # artifactは記憶領域。ジョブ間でのデータ共有手段。今回はimage名を次のJobで使用するために保存する
      - name: Upload the image uri file as an artifact
        uses: actions/upload-artifact@v4
        with:
          name: api-image-uri
          # usesの中ではワーキングディレクトリは効かないので、ルートパスから入れる必要がある。
          path: api/api-image-uri.txt

  # Deploy
  deploy:
    runs-on: ubuntu-latest
    # needsは `test-and-build`のjobが完了していることが必須という意味。deployの前に走る必要がある。needsは複数入れることも可能。
    needs: [test-and-build]

    steps:
      - name: Checkout
        # `actions/checkout@v4`の役割: リポジトリのコードをワークフローの実行環境にダウンロード（チェックアウト）する
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        # https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ env.AWS_REGION }}
          # 秘匿性の高い情報を扱いたいときはsecretsという別の領域を使う。githubのsettingで定義する。
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}

      - name: Download the artifact
        uses: actions/download-artifact@v4
        with:
          name: api-image-uri
          # ここでのpathはダウンロード先のpathになる。api-image-uriの中のデータがartifacts野中にダウンロードされる。
          path: artifacts

      - name: Define the image URI
        # `$GITHUB_ENV`はGitHub Actionsの特殊なファイルで、このファイルに書き込むことで環境変数を設定できます。
        # API_IMAGE_URIが以降で使えるようになる。
        run: |
          echo "API_IMAGE_URI=$(cat artifacts/api-image-url.txt)" >> $GITHUB_ENV

      - name: Fill in the new image URI in the amazon ECS task definition
        # idが設定されたステップの出力は、steps.[id].outputs
        id: render-task-def
        # https://github.com/marketplace/actions/amazon-ecs-render-task-definition-action-for-github-actions
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION_API }}
          container-name: api
          image: ${{ env.API_IMAGE_URI }}

      - name: Deploy ECS task
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          # サービスが正常に更新されたことを確認してから次のステップに進みます
          wait-for-service-stability: true
```

### `task-def-api.json`ファイルの作成
`my-app-api:1`に移動し、Jsonをコピー。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/a96c9f50-5f7e-4bc9-82bf-d7a9c4e2850a.png)

```
{
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:032559375316:task-definition/my-app-api:1",
    "containerDefinitions": [
        {
            "name": "dummy",
            "image": "public.ecr.aws/nginx/nginx:stable-perl",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "dummy-80-tcp",
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/my-app-api",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "systemControls": []
        }
    ],
    "family": "my-app-api",
    "executionRoleArn": "arn:aws:iam::032559375316:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "revision": 1,
    "volumes": [],
    "status": "ACTIVE",
    "requiresAttributes": [
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.28"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        }
    ],
    "placementConstraints": [],
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "512",
    "memory": "1024",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "registeredAt": "2025-05-25T04:47:34.560Z",
    "registeredBy": "arn:aws:iam::032559375316:user/main",
    "enableFaultInjection": false,
    "tags": []
}
```

そのままだとgithub actionsで失敗してしまうので、以下のように加工する。(不要なものを削除)
```
{
  "containerDefinitions": [
      {
          "name": "api",
          "image": "<image uri>",
          "cpu": 0,
          "portMappings": [
              {
                  "name": "api-8080-tcp",
                  "containerPort": 8080,
                  "hostPort": 8080,
                  "protocol": "tcp",
                  "appProtocol": "http"
              }
          ],
          "essential": true,
          "environment": [],
          "environmentFiles": [],
          "mountPoints": [],
          "volumesFrom": [],
          "ulimits": [],
          "logConfiguration": {
              "logDriver": "awslogs",
              "options": {
                  "awslogs-group": "/ecs/my-app-api",
                  "mode": "non-blocking",
                  "awslogs-create-group": "true",
                  "max-buffer-size": "25m",
                  "awslogs-region": "ap-northeast-1",
                  "awslogs-stream-prefix": "ecs"
              },
              "secretOptions": []
          },
          "systemControls": []
      }
  ],
  "family": "my-app-api",
  "executionRoleArn": "ecsTaskExecutionRole",
  "networkMode": "awsvpc",
  "volumes": [],
  "placementConstraints": [],
  "requiresCompatibilities": [
      "FARGATE"
  ],
  "cpu": "512",
  "memory": "1024",
  "runtimePlatform": {
      "cpuArchitecture": "X86_64",
      "operatingSystemFamily": "LINUX"
  }
}
```

コミットしてpushするとdeployのjobでcicdが落ちる。理由はIAMの権限が足りないから。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/400593c9-3e2b-4cbe-bbdd-44c905e4fd58.png)

Open ID Connectのために作ったロールである`GitHubActionsEcsLearning`にiam:PassRoleというアクションを実行する権限が足りないという意味のエラー。
`"executionRoleArn": "ecsTaskExecutionRole"`の権限を与えるという権限も非常に重要な力を持った権限。

「ロールを渡す（PassRole）」 = あるAWSサービスが、別のサービスやリソースに対して「このロールを使って動作してください」と指定すること。

**例1: ECSでの「ロール渡し」**
```
GitHubActions → ECS → コンテナ
     ↓          ↓        ↓
(PassRole)    (使用)    (実行)
```
1. GitHubActionsが「ECSタスクを作成してください」とリクエスト
2. その際に「このタスクはecsTaskExecutionRoleを使って実行してください」とロールを渡す
3. ECSがそのロールを使ってタスクを実行

```
1. GitHubActions（GitHubActionsEcsLearning role）
   ↓ "ecsTaskExecutionRoleを使ってタスクを実行して"
2. ECS Service
   ↓ ecsTaskExecutionRoleを使用
3. コンテナ起動・実行
```

### IAMロールの編集 `GitHubActionsEcsLearning `
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/e344bcf5-7e73-447d-9529-25e92a683cce.png)

編集をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/7028f0db-194d-47b3-a7ad-6e82e3134d9e.png)

IAMを選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/844510b2-50cc-4d7a-90c1-11de461e4daa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/96196fe2-7d1a-4197-8844-d58b5de2b7e3.png)
次へ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/cbba3362-f604-4af1-97d1-caa46ac4defd.png)

ここまでやったら、Github Actionsを再度実行する。
Re-run this jobをクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/4b50e1e6-e373-4553-a0d8-1855f827fd8b.png)

成功しました。が、アクセスができない問題が残っている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/2bbd0072-f896-4b87-bd89-d06348e746c2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/0ba7dc10-9db1-4838-9926-e5a467abaa65.png)

以下でアクセスすると、グルグルが終わらない。セキュリティーグループの設定が今足りていないため、ファイヤーウォールでブロックされている。
http://52.194.185.72:8080/api/hello

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/e23f1ff2-153c-469a-bc6a-c3a6e4ba6004.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/72c903ca-af00-4b1b-825b-6ca65a0654d4.png)

もう一度叩く
http://52.194.185.72:8080/api/hello

成功しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4077353/34fcb174-18b7-4bc7-af1a-5d6f6f653790.png)