1. はじめに
GitHub Actionsを使い、Amazon ECSへ自動デプロイする際の設計パターンをまとめます。本稿では、ECRとECSがすでに構築されている前提で話を進めます。GitHub Actionsによるビルドからデプロイまでの流れのうち、CIの部分は省略し、デプロイに関わる設計に絞って説明します。GitHub Actionsのyaml記法を細かく解説することではなく、デプロイという一連の処理をどう設計すればよいか、その考え方を理解し、再現できるようにすることを目的とします。
2. 全体像
まずは、デプロイの一連の流れを確認していきましょう。粗い粒度で全体像を抑えるようにすると、以下のようになります。ECRにイメージをpushし、そのイメージを使うようタスク定義を更新して、ECSにデプロイします。
- 認証(OIDCでAWSに一時的な認証情報を発行してもらう。)
- レジストリログイン (発行された認証情報を使い、コンテナレジストリ(ECR)にログインする)
- イメージのビルド・push(アプリケーションのDockerイメージをビルドし、タグを付けてECRにpushする)
- タスク定義の更新(pushした新しいイメージを指すタスク定義を作成し、新しいリビジョンとして登録する)
- デプロイ(新しいリビジョンを使うようECSサービスを更新し、ロールアウトが安定するまで待つ)
もう少し詳細にみていきましょう。GitHub Actionsのyamlファイルに処理として記載する粒度で書くと、以下のように分けられます。ECSにデプロイする比較的標準的なフローです。
- ソース取得(checkout)
- 事前検証(必要なSecrets/Variablesが揃っているか。無いまま進んで中盤で失敗するより早期に落とす)
- コミットSHAの取得(イメージタグ付け・通知に使う)
- AWS認証(OIDC)
- コンテナレジストリへのログイン
- イメージのビルド
- イメージのpush(タグ戦略に基づき1つ以上のタグを付与。)
- タスク定義の更新・新リビジョン登録
- デプロイ実行(サービス更新など、環境の種類に応じて内容が変わる)
- デプロイ完了待ち(ロールアウト監視)
- 結果通知(成功/失敗)
それぞれのステップが何をしているかを順番に説明します。
最初に、リポジトリのソースコードをcheckoutで取得します(①)。次に、必要なSecretsやVariablesが揃っているかを事前に検証します(②)。ここで不足に気づけると、ビルドの途中で失敗するより早く対処できます。続いて、コミットSHAを取得します(③)。このSHAは、あとのイメージタグ付けや通知、デプロイのスキップ判定に使います。
ここまでの準備が整ったら、OIDCでAWSに認証し(④)、ECRにログインします(⑤)。次に、アプリケーションのイメージをビルドします(⑥)。タグ戦略に基づいたタグを付けて、ECRにpushします(⑦)。
イメージの準備ができたら、そのイメージを指すタスク定義を新しいリビジョンとして登録します(⑧)。ECSのタスク定義はイミュータブルなので、イメージを差し替えるだけでも登録し直す必要があります。続いて、そのリビジョンを使うようサービスを更新し、デプロイを実行します(⑨)。デプロイは実行した時点で終わりではありません。ロールアウトが安定するまで待つことで、新しいリビジョンが正常に動いていることを確認できます(⑩)。
最後に、デプロイの成功・失敗をチャットなどに通知し、一連の処理を終えます(⑪)。
3. 設計のポイント
3章では、自動デプロイ手順を設計するときに、押さえておきたいポイントを記載していきます。
3.1 トリガー設計:いつ・どうやって実行するか
トリガー設計のポイントは、何をいつきっかけに実行するか、です。GitHub Actionsでは複数のトリガー設定ができるので、環境や目的ごとに柔軟に設計できます。
GitHub Actionsの代表的なトリガーには、以下の4種類があります。これらは1つに絞る必要はなく、目的に応じて組み合わせて使えます。時刻指定と手動実行を両方設定など、複数のトリガーを設定することもできます。
- push・pull_request:ブランチへの操作をきっかけに自動実行する
- workflow_dispatch:画面上のボタン操作で手動実行する
- schedule:cron形式で時刻を指定し、定期的に実行する
- workflow_run:他のワークフローの完了をきっかけに実行する
pushトリガーを使う場合は、対象ブランチを絞り込む必要があります。全てのブランチへのpushに反応してしまうと、意図しないタイミングでデプロイが走ってしまいます。「リリースブランチにpushされたとき」など、用途によって指定しましょう。
手動実行を許可する場合は、実行時に何を入力させるかもポイントになります。inputsは、GitHub Actionsの画面で実行ボタンを押すときに、値を指定できる仕組みです。なお、どのブランチから実行するかは標準の選択欄で指定できるため、inputsを定義する必要はありません。inputsが役に立つのは、実行するブランチとは別の情報を渡したい場合です。例えば、デプロイしたいイメージタグをinputsで受け取る設計にできます。過去のバージョンを指定して戻したいときにも使えます。
また、同じ環境に対して複数のデプロイが同時に走ると、タスク定義の登録やサービス更新が競合するおそれがあります。そのため、concurrencyで同一環境へのデプロイを直列化しておくとよいでしょう。グループのキーには、ブランチ名ではなくデプロイ先の環境名を使います。また、実行中のデプロイを途中で打ち切ると、ECS側のロールアウトだけが進んでしまいます。そのため、実行中のデプロイは取り消さず、後続を待たせる設定にしておきます。
3.2 認証設計:なぜOIDCか
認証設計のポイントは、GitHub ActionsからAWSへどう認証情報を渡すか、です。方法には大きく2つあります。1つはIAMユーザーの長期アクセスキーを使う方法、もう1つはOIDCを使う方法です。
| 方式 | 認証情報 | 漏洩時のリスク | ローテーション | 初期設定 |
|---|---|---|---|---|
| 長期アクセスキー | IAMユーザーのアクセスキーをSecretsに保存する | 漏洩すると長期間悪用されるおそれがある | 定期的な手動ローテーションが必要 | シンプル |
| OIDC | 実行のたびに短命なトークンを発行する | 漏洩しても影響範囲が小さい | 不要 | 信頼ポリシーの設定が必要でやや複雑 |
長期アクセスキーは、設定がシンプルというメリットがあります。しかし、Secretsが漏洩すると長期間悪用されるおそれがあります。ローテーションの運用も、別途仕組みを用意する必要があります。
一方、OIDCは実行のたびに短命な認証情報を発行する仕組みです。そのため、漏洩そのものが起こりにくく、ローテーションも不要になります。初期設定ではIAM側に信頼ポリシーを設定する必要があり、長期アクセスキーよりやや手間がかかりますが、セキュリティの視点では、基本的にOIDCを採用するのがよいでしょう。
OIDCを採用する場合、インフラチームにIAMロールを作成してもらい、そのARNをGitHubのSecretsに設定して利用します。信頼ポリシーや最小権限の考え方については、付録を参照してください。
3.3 タスク定義の設計:なぜ新リビジョン登録が必要か
タスク定義の設計で押さえておきたいのは、なぜイメージを差し替えるだけで新しいリビジョンの登録が必要なのか、という点です。
ECSのタスク定義はイミュータブル(不変)です。一度登録した内容を書き換えることはできず、変更したい場合は新しいリビジョンとして登録し直すしかありません。ここでつまずきやすいのが、ECRへのpushだけで反映されると考えてしまうことです。:latestのようなmutableタグを使っていても、ECSが自動的に新しいイメージを検知することはありません。
新しいイメージを反映させるには、2段階の操作が必要です。まず、新しいイメージを指すタスク定義を新しいリビジョンとして登録します。次に、そのリビジョンを使うようサービスを更新します。
Herokuのように、gitへpushすればビルドから反映まで自動で進むPaaSもあります。こうしたサービスに慣れていると、ECSでも同じ感覚で捉えてしまいがちです。ECSでは、この2段階の手続きを意識して設計に組み込む必要があります。ここをしっかり認識しておかないと、デプロイはしたけど、コードが更新されていないという事象が起きます。
3.4 デプロイ完了の判定:なぜ安定待ちが必要か
デプロイ完了の判定で押さえておきたいのは、何をもって「デプロイが完了した」とみなすか、という点です。
GitHub Actionsのワークフローは、サービスの更新を実行した時点では終わりません。実際には、そのあとのロールアウトが安定するまで待つ必要があり、この処理に最も時間がかかります。ここでいうロールアウトとは、新しいタスク定義に基づいてタスクを入れ替えていく一連の処理のことです。ECSは新しいタスクを起動し、ヘルスチェックが通ったことを確認してから、古いタスクを順次停止します。
安定するとは、新しいタスクの起動数が希望する数に達した状態を指します。加えて、ヘルスチェックを通過し、古いタスクの停止も完了している必要があります。イメージの取得やアプリケーションの起動、ヘルスチェックの判定など、複数の要因が絡みます。そのため、ここが最も時間のかかるところです。
安定を待たずにワークフローを終了させるとどうなるでしょうか。新しいタスクの起動に失敗していても、デプロイは成功したものとして扱われてしまいます。そのため、安定するまで待つ処理を、デプロイの一部として組み込んでおく必要があります。
3.5 失敗時の設計:なぜ通知が必要か
失敗時の設計で押さえておきたいのは、デプロイの結果をどう可視化するか、という点です。
GitHub Actionsの実行結果は、GitHub上で確認できます。しかし、能動的に見に行かない限り、失敗に気づくのが遅れてしまいます。そのため、成功・失敗をチャットなどに通知する仕組みを用意しておくとよいでしょう。
ここで注意したいのは、「失敗に気づける」設計と、「失敗したら自動で元に戻る」設計は別物だという点です。通知を用意しても、それは気づけるようにするだけです。前のタスク定義リビジョンへ戻すには、ECS側でデプロイサーキットブレーカーを有効にしておく必要があります。有効にしていなければ、ロールアウトが失敗しても自動では戻りません。自動ロールバックの詳細は、別記事で扱います。
4. つまずきポイント集
つまずきやすいポイントを、注意が必要な順に挙げます。
1つ目は、デプロイに失敗しても自動では戻らないことです。ECS側でデプロイサーキットブレーカーを有効にしていなければ、前のリビジョンへ自動的に戻る処理は動きません。GitHub Actionsのワークフロー側にも、戻す処理は含まれていません。対応案は別記事で扱います。
2つ目は、IAM信頼関係のcondition設定の書き間違いです。OIDCの信頼ポリシーを誤って設定すると、認証の時点でつまずきます。リポジトリ名やEnvironment名のスペルミスがないか、よく確認しましょう。
3つ目は、タスク定義を再登録するときに、除去が必要なフィールドを消し忘れることです。稼働中の定義をそのまま使うと、revisionやstatusなど自動採番されるフィールドが含まれます。これらを除去しないと、登録時にエラーになります。なお、公式アクションを使う場合はこれらが自動で除去されるため、CLIで手動登録するときの注意点になります。
4つ目は、ECRログインとリージョンの不一致です。認証したリージョンとECRリポジトリのリージョンが異なると、ログインは成功してもpush・pullに失敗します。
5つ目は、ロールアウトが終わらないケースです。ヘルスチェックの設定やセキュリティグループ、リソースの割り当てなどを確認しましょう。
5. おわりに
本稿では、トリガー・認証・タスク定義の更新・デプロイ完了の判定・失敗時の通知という一連のポイントを整理しました。これらを押さえておけば、教科書的な自動デプロイの構成を再現できるはずです。次回の投稿では、タグ戦略や環境ごとの違いといった実務寄りの設計判断やバッチ特有の注意点を扱います。
最後まで読んでいただいた方、ありがとうございました。
付録:IAM権限の参考例
ロールに関する設定
次の3つのロールに整理しました。
- OIDC用デプロイロール:GitHub ActionsがOIDCでAssumeし、ECRへのpush・ECSタスク定義の登録・サービス更新を行うためのロール。インフラチームに作成を依頼する対象
- ECSタスク実行ロール:ECSがコンテナ起動時に、イメージのpullやログ出力などに使うロール
- ECSタスクロール:コンテナ内のアプリケーションが、AWS APIを呼び出す場合に使うロール
3つのロールは、それぞれ独立して存在します。権限ポリシーのPassRoleは、デプロイロールが持つ「このロールなら他サービスに渡してよい」という許可リストのようなものです。これにより、権限昇格を防いでいます。
このうち、ECSタスク実行ロールとECSタスクロールは、すでに構築されているECS環境の一部として存在している想定です。新たにインフラチームへ作成を依頼するのは、OIDC用デプロイロールです。信頼ポリシーと権限ポリシーは、以下を目安にしてください。
信頼ポリシー:
- Principal: GitHub OIDC provider(
token.actions.githubusercontent.com) - Condition例:
repo:{org}/{repo}:environment:{env名}(ブランチ縛りならref:refs/heads/{branch})
権限ポリシー(最小権限の目安):
- ECR:
GetAuthorizationToken/BatchCheckLayerAvailability/InitiateLayerUpload/UploadLayerPart/CompleteLayerUpload/PutImage/BatchGetImage/DescribeImages - ECS:
DescribeServices/DescribeTaskDefinition/RegisterTaskDefinition/UpdateService - IAM:
PassRole(ECSタスク実行ロール・タスクロールに対して。忘れるとregister-task-definitionが通らない)
依頼時には、リポジトリ名・Environment名・対象ブランチ・上記API一覧を合わせて伝えます。あわせて、ECR/ECSのクラスター・サービス・タスク定義がすでに存在するか、無ければ誰が作るかも確認しておきましょう。
GitHub側の設定
GitHub側の設定についても整理します。Environmentsは、環境ごとにVariablesやSecrets、承認ルールなどをまとめて管理できる仕組みです。production環境などの単位で作成します。
Variablesは、リージョン名やリソース名など、非機密な値を保存する仕組みです。ワークフローからは${{ vars.変数名 }}の形式で参照します。Secretsは、暗号化して保存する機密情報です。ワークフローからは${{ secrets.変数名 }}の形式で参照します。
- Environments作成
- Variables:
AWS_REGION/ECR_REPOSITORY/ECS_CLUSTER/ECS_SERVICE/CONTAINER_NAME - Secrets:
AWS_DEPLOY_ROLE_ARN(チャット通知を実装する場合は、通知用のWebhook URLも追加する)
付録:yamlファイルのサンプルコード
# ==============================================================================
# サンプル: GitHub Actions による ECS へのデプロイ(一般的な構成)
# ------------------------------------------------------------------------------
# 前提:
# - ECR リポジトリ、ECS クラスター/サービス/タスク定義は作成済み
# - タスク定義は本リポジトリ内に JSON として管理している想定
# (実運用ではインフラチームと管理方針をすり合わせること)
# - OIDC 用 IAM ロールが GitHub Actions から Assume できるように設定済み
#
# GitHub 側の事前準備(Settings → Environments → <環境名>):
# Variables:
# - AWS_REGION 例: ap-northeast-1
# - ECR_REPOSITORY 例: sample-app/web
# - ECS_CLUSTER 例: sample-ecs-cluster
# - ECS_SERVICE 例: sample-ecs-service
# - CONTAINER_NAME 例: web(タスク定義内のコンテナ名)
# Secrets:
# - AWS_DEPLOY_ROLE_ARN OIDCでAssumeするIAMロールのARN
#
# 注意(本サンプルの限界):
# このワークフロー自体には、自動ロールバックの処理が無い。
# 前のリビジョンへ自動で戻したい場合は、ECS サービス側で
# デプロイサーキットブレーカー(rollback: true)を有効にしておく。
# 有効にしていない場合は、通知を受けて手動で戻す運用になる。
# ==============================================================================
name: Deploy to ECS (sample)
on:
push:
branches:
- main
workflow_dispatch:
permissions:
id-token: write # OIDC トークン発行に必須
contents: read
concurrency:
group: deploy-ecs-production # デプロイ先の環境ごとにグループを分ける
cancel-in-progress: false # 実行中のデプロイは取り消さない
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
environment: production # 環境名に合わせて変更
env:
AWS_REGION: ${{ vars.AWS_REGION }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
ECS_SERVICE: ${{ vars.ECS_SERVICE }}
CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate required variables
run: |
if [ -z "${{ vars.AWS_REGION }}" ] || [ -z "${{ vars.ECR_REPOSITORY }}" ] || \
[ -z "${{ vars.ECS_CLUSTER }}" ] || [ -z "${{ vars.ECS_SERVICE }}" ] || \
[ -z "${{ vars.CONTAINER_NAME }}" ]; then
echo "必要なVariablesが設定されていません。"
exit 1
fi
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
id: build-image
env:
ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker buildx build \
--platform linux/amd64 \
--cache-from type=gha \
--cache-to type=gha,mode=max \
-f Dockerfile \
-t "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}" \
--push \
.
echo "image=${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
# タスク定義はリポジトリ内(task-definition.json)で管理する想定。
# 現在稼働中の定義から取得する運用にする場合は describe-task-definition に置き換える。
- name: Fill in the new image ID in the task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
cluster: ${{ env.ECS_CLUSTER }}
service: ${{ env.ECS_SERVICE }}
wait-for-service-stability: true
- name: Notify on success
if: success()
run: echo "デプロイに成功しました(commit: ${{ github.sha }})。"
# ------------------------------------------------------------------
# ワークフロー側では戻さない。失敗時は通知のみ。
# ECS のデプロイサーキットブレーカーが無効な場合は、手動で戻す手順を
# 運用ドキュメント側に明記しておくこと。
# ------------------------------------------------------------------
- name: Notify on failure
if: failure()
run: echo "デプロイに失敗しました(commit: ${{ github.sha }})。ロールアウトの状態を確認してください。"
参考文献
[GitHub Docs] Github 「Amazon Elastic Container Serviceへのデプロイ」
https://docs.github.com/ja/actions/how-tos/deploy/deploy-to-third-party-platforms/amazon-elastic-container-service
2026-09-15アクセス.
[技術評論社] 野村 友規さま (2024-05-29)「GitHub CI/CD実践ガイド――持続可能なソフトウェア開発を支えるGitHub Actionsの設計と運用 (エンジニア選書)」
https://www.amazon.co.jp/dp/4297141736
2026-09-15アクセス.