はじめに
前回の記事ではフルスタック構成(Amplify + CloudFront + ECS Fargate + RDS)を動作確認した。毎回デプロイするたびに以下のコマンドを手動で叩いていた。
# ECR にログイン
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin <ECR_URL>
# ビルド & プッシュ
docker buildx build --platform linux/amd64 -t <ECR_URL>:latest --push backend/
# ECS を再デプロイ
aws ecs update-service \
--cluster movielogrecord-cluster \
--service movielogrecord-backend \
--force-new-deployment
今回はこれを git push するだけで自動実行される CI/CD パイプラインに置き換えた。
認証方式として OIDC(OpenID Connect)を採用した。「アクセスキーを使えばいいのでは?」と思う人も多いと思うので、なぜ OIDC なのかを重点的に説明する。
アクセスキー方式の問題点
GitHub Actions から AWS を操作する一番単純な方法は、IAM ユーザーのアクセスキーを GitHub Secrets に登録することだ。
# アクセスキー方式(非推奨)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
この方式には根本的な問題がある。
長期クレデンシャルのリスク
アクセスキーは有効期限がない。一度漏れると、削除するまでずっと悪用可能な状態が続く。漏れる経路はいくつもある:
-
git logに誤ってコミットしてしまう - GitHub のログやデバッグ出力に表示される
- リポジトリが public になった瞬間にスキャンされる
GitHub は push 時にアクセスキーのパターンを自動検知して警告を出すが、あくまで後手の対策だ。
最小権限の管理が難しい
アクセスキーは「IAM ユーザー」に紐づく。そのユーザーに必要以上の権限を与えてしまうと、キーが漏れたときの被害範囲が広がる。CI/CD 専用ユーザーを作って権限を絞ることはできるが、管理が煩雑になる。
OIDC 方式とは何か
OIDC(OpenID Connect)を使うと、アクセスキーを一切使わずに AWS の権限を取得できる。
仕組みを一言で言うと「GitHub が発行した身分証明書を AWS に提示して、一時的な通行証をもらう」イメージ。
トークン交換の流れ
① GitHub Actions ワークフローが起動
↓
② GitHub が OIDC トークン(JWT)を発行
(中身: リポジトリ名・ブランチ名・コミット SHA などを含む短命トークン)
↓
③ aws-actions/configure-aws-credentials が
AWS STS に AssumeRoleWithWebIdentity リクエストを送る
↓
④ AWS が「このトークンは本当に GitHub が発行したか?」を検証
(GitHub の公開鍵で署名を確認)
↓
⑤ IAM ロールの Condition(リポジトリ・ブランチ)が一致するか確認
↓
⑥ 一時クレデンシャル(有効期限1時間)を返す
↓
⑦ 以降の AWS 操作はこの一時クレデンシャルで実行
アクセスキーとの比較
| アクセスキー | OIDC | |
|---|---|---|
| 有効期限 | なし(手動で削除するまで有効) | 約1時間(自動失効) |
| 漏洩リスク | 高い(Secrets に保存が必要) | 低い(Secrets に保存不要) |
| 権限の紐づけ | IAM ユーザー | IAM ロール(条件付き) |
| 特定リポジトリへの制限 | 難しい | 可能(Condition で指定) |
| AWS 推奨 | 非推奨 | 推奨 |
OIDC の最大のメリットは「漏れても1時間で無効になるトークンしか使わない」こと。そもそも保存するものがないので漏れようがない。
Terraform での実装
OIDC を使うには AWS 側に2つのリソースが必要だ。
① OIDC プロバイダー
GitHub の OIDC エンドポイントを AWS に「信頼できる発行元」として登録する。
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
thumbprint_list は GitHub の TLS 証明書のフィンガープリント。AWS がトークンの検証に使う。
② IAM ロール(信頼ポリシー)
どの GitHub リポジトリ・ブランチからの assume を許可するかを Condition で絞る。
resource "aws_iam_role" "github_actions" {
name = "movielogrecord-github-actions-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github_actions.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# master ブランチの push のみ許可
"token.actions.githubusercontent.com:sub" = "repo:soh506/movielogrecord-ver2:ref:refs/heads/master"
}
}
}]
})
}
sub(subject)クレームにリポジトリ名とブランチ名を指定することで、このリポジトリの master ブランチからのワークフローだけがこのロールを assume できる。他のリポジトリや PR ブランチからは assume できない。
③ 権限ポリシー
ロールに必要最小限の権限だけ付与する。
# ECR へのイメージ push 権限
resource "aws_iam_role_policy_attachment" "github_actions_ecr" {
role = aws_iam_role.github_actions.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}
# ECS の再デプロイ権限のみ(UpdateService と DescribeServices だけ)
resource "aws_iam_role_policy" "github_actions_ecs" {
name = "movielogrecord-github-actions-ecs-policy"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["ecs:UpdateService", "ecs:DescribeServices"]
Resource = aws_ecs_service.backend.id # このサービスだけ
}]
})
}
ECS 権限は ecs:* ではなく UpdateService と DescribeServices だけに絞った。最小権限の原則。
GitHub Actions ワークフロー
name: Deploy Backend
on:
push:
branches: [master]
paths:
- 'backend/**' # backend/ 配下に変更があった push だけ発火
env:
AWS_REGION: ap-northeast-1
ECR_REPOSITORY: movielogrecord-backend
ECS_CLUSTER: movielogrecord-cluster
ECS_SERVICE: movielogrecord-backend
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC トークンの取得に必要
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker buildx build \
--platform linux/amd64 \
-t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_REPOSITORY:latest \
--push \
backend/
- name: Force new ECS deployment
run: |
aws ecs update-service \
--cluster $ECS_CLUSTER \
--service $ECS_SERVICE \
--force-new-deployment
各ポイントの解説
paths: ['backend/**']
frontend/ や infra/ だけを変更したときにバックエンドの無駄なデプロイが走らないようにする。
permissions: id-token: write
このジョブが OIDC トークンを取得する権限を明示的に宣言している。これがないと OIDC による認証ができない。デフォルトでは read 権限しかないため明示が必要。
IMAGE_TAG: ${{ github.sha }}
コミット SHA をタグに使うことで、どのコミットのイメージが ECS で動いているかを追跡できる。latest だけだとどのコードが動いているか分からなくなる。
--platform linux/amd64
GitHub Actions のランナーは x86_64 なので本来不要だが、明示しておくことで意図を伝える(前回の記事で M2 Mac ローカルビルドとの違いがあった部分)。
使い方(実際にデプロイするとき)
-
terraform applyを実行 -
terraform output github_actions_role_arnでロールの ARN を取得 - GitHub リポジトリの Settings → Secrets and variables → Actions →
AWS_ROLE_ARNとして登録 -
backend/配下を変更して master に push → 自動デプロイが走る
まとめ
| 項目 | 内容 |
|---|---|
| 認証方式 | OIDC(アクセスキー不要) |
| 発火条件 | master への push かつ backend/ に変更あり |
| IAM 権限 | ECR push + ECS UpdateService のみ(最小権限) |
| Terraform 管理 | OIDC プロバイダー・IAM ロール・ポリシーをコード化 |
OIDC の仕組みを理解すると「なぜクレデンシャルを Secrets に入れなくていいのか」がスッキリする。短命トークンの交換という概念は、JWT 認証を実装したこのプロジェクトとも通じる部分がある。
これでこのシリーズは完結。Django の学習用アプリから始まり、Next.js + DRF + JWT + Terraform + AWS + CI/CD まで一通り実装できた。
個人的感想(ポエム)
ものすごく複雑な気持ちでこれを書いている。このシリーズは7回も続いたが、すべてClaude Codeがやったことだ。コードも記事もすべて任せた。一丁前にわかってる風に書いてるが、クロードさんがやったことで、もちろん自分の素の実力ではないし、出てきた技術や知識も身についてない。しかししかしだ。こうして成果・結果は出てる。この事実をどう消化したものか思い悩んでいる。私はただの業界未経験の素人で、プログラマーでもエンジニアでもない。そんな私ですら、AIの力を借りればここまでできる。それも含めての自分の実力なのか、そんなことを気にする必要はない、出した結果や成果がすべてなのか、いやいや0から自分でコードが書けてこそ本物なのか。確かに0から書けてこそとも思うが、自分の力だけでこのシリーズを完結できたかというと絶対できなかっただろうなという確信がある。バグったらどう直していいかわからず途方に暮れてやがて心折れて放置するのが容易に想像つくからだ。AIを使うとあまりにも効率が良すぎてどんどん差がつきすぎて、できなかったであろう自分と、現在のできてしまった自分との差に絶望すら感じてしまう。5年前にDjangoに真正面からがっぷり四つに組んで、ああでもないこうでもないと格闘して仕組みがわかって自分でコードが書けて上手く動いたときの喜びと手応え!その味を知ってしまっているだけに、成果を出したといってもこの味気なさに正直気持ちが萎えている。世の中のプログラマーやエンジニアはもうすでにAIでガンガンに仕事を進めてるんだろうが、このあたりどう感じているんだろうか。答えがないのもわかっちゃいるが、気持ちの折り合いがついてないしまだ開き直れない。と泣き言も記しておく。