前回は、AWS を管理・共通・確認用・本番の4つのアカウントに分け、社員は Google Workspace のアカウントでログインする形にしました。

今回は、プログラムからの反映の入り口です。GitHub Actions(=GitHub 上でテストや反映を自動で行う仕組み)から AWS に入るとき、昔は AWS のアクセスキーを GitHub に保存する方法がよく使われていました。この連載では、アクセスキーをどこにも置かず、OIDC で実行のたびに短い時間だけ使える権限をもらう 形にします。
「GitHub に AWS のキーを保存するのは怖い。でも、キーなしでどうやって反映するの?」
結論から言うと、GitHub Actions は実行のたびに「自分はどのリポジトリの、どの Environment のジョブか」を名乗る証明書のようなもの(OIDC のトークン)を受け取れます。AWS 側に「この名乗りなら、このロールに入ってよい」という条件を置けば、キーなしで入れます。ただし、新しい GitHub の組織では名乗りの形が変わっていて、よく見る例のとおりに条件を書くと断られます。この落とし穴も含めて説明します。
アクセスキーを置かない仕組み
OIDC(OpenID Connect)は、ログインの結果を別のサービスに安全に伝えるための決まりごとです。GitHub Actions と AWS の間では、次の流れになります。
- ジョブが GitHub から OIDC のトークンを受け取る(中身に「どのリポジトリ・どの Environment・どのブランチやタグか」が書いてある)
- ジョブがそのトークンを AWS に見せ、ロール(=権限のまとまりを持った「役」)に入りたいと頼む
- AWS は、ロールに書かれた条件(どのリポジトリ・どの Environment なら入ってよいか)とトークンの中身を比べ、一致すれば短時間だけ使える権限を渡す
長く使えるキーがどこにもないので、漏れる心配がありません。GitHub と AWS の両方が、この形を勧めています。
📰 出典:Configuring OpenID Connect in Amazon Web Services(GitHub Docs)
最初の準備は、人が CloudShell で1回だけ
AWS 側の受け口とロールは、人が各アカウントの CloudShell(=ブラウザの中で使える AWS 用のターミナル)で1回だけ作りました。当社は手順を1つのスクリプトにまとめ、次の3つを行いました。
| 作るもの | 中身 |
|---|---|
| OIDC の受け口(ID プロバイダー) | token.actions.githubusercontent.com からのトークンを信頼する設定 |
| 反映用のロール | GitHub Actions が入るロール。条件とできることを絞る(下で説明) |
CDK の準備(cdk bootstrap) | CDK(=AWS の構成をコードで書く道具。第4回)が反映に使う置き場とロールを作る |
スクリプトは最初にアカウント番号を確かめ、違うアカウントで実行されたら止まるようにしました。CloudShell には非公開のリポジトリを持ち込む手間がかかるので、1ファイルで完結させています。
画面で作る場合は、IAM の画面の「Identity providers」から「Add provider」を選び、次のように入れます(画面の言語:英語。日本語表示では名前が異なる場合があります)。
| 項目 | 入れるもの |
|---|---|
| Provider type | OpenID Connect |
| Provider URL | https://token.actions.githubusercontent.com |
| Audience | sts.amazonaws.com |
以前の手順では証明書の拇印(thumbprint)を入れる必要がありましたが、今は不要です。
📰 出典:aws-actions/configure-aws-credentials(GitHub)
CDK の準備は、反映するアカウントとリージョンごとに1回、次のコマンドで行います。
npx aws-cdk@2 bootstrap aws://<アカウント番号>/ap-northeast-1
ロールの条件の決め方
この回で作るのは、ロールの条件とできることの見本と、反映のワークフローです。プロジェクトに infra フォルダを作って、見本を置きます。
mkdir -p infra
入ってよい条件(信頼ポリシー)
ロールに「誰が入ってよいか」を書いたものを、信頼ポリシーと呼びます。本番のロールの例です。
infra/oidc-trust.example.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<アカウント番号>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<組織名>@<組織の番号>/<リポジトリ名>@<リポジトリの番号>:environment:production"
}
}
}
]
}
aud(宛先)がsts.amazonaws.comであることsub(名乗り)が「このリポジトリの、production の Environment のジョブ」であること
sub を Environment で絞るのがポイントです。GitHub の Environment(=反映先ごとの設定のまとまり)には「どのブランチやタグから使えるか」を決める設定があります。本番のロールには本番の Environment のジョブからだけ入れるようにし、本番の Environment は v で始まるタグからしか使えないようにします。こうすると、人がタグを作らない限り、本番には反映されません。
できること(権限ポリシー)
反映用のロールに許すのは、「CDK の準備で作られたロールに乗り換えること」だけにしました。実際に AWS のものを作るのは、乗り換えた先のロールです。
infra/deploy-role-policy.example.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<アカウント番号>:role/cdk-*"
}
]
}
GitHub Actions のロールに広い権限を直接付けないことで、万一ワークフローが乗っ取られても、できることを CDK の反映に限れます。当社の実際の構成では、コンテナイメージを置くロールを別に作り、main のジョブからだけ・その置き場への書き込みだけに絞っています。
落とし穴:新しい組織では、名乗り(sub)が番号付きだった
ここが今回いちばんつまずいたところです。
よく見る sub の例は、次の形です。
repo:組織名/リポジトリ名:environment:staging
ところが、当社の新しい GitHub の組織のリポジトリでは、実際の名乗りは次の形でした。
repo:組織名@組織の番号/リポジトリ名@リポジトリの番号:environment:staging
よく見る例のとおりに条件を書いたため一致せず、AWS へのログインが断られ続けました。ワークフローで、受け取ったトークンの中の sub だけを表示して(トークンそのものは表示しないように注意して)、はじめて形の違いに気付きました。
GitHub のドキュメントによると、2026年7月15日以降に作られたリポジトリでは、名乗りに組織とリポジトリの番号が入る形(immutable subject claims)が既定になっています。名前は付け直したり、別の人が同じ名前を使ったりできますが、番号は変わりません。そのため、番号付きの形はかえって安全 です。
📰 出典:OpenID Connect reference – Immutable subject claims(GitHub Docs)
組織とリポジトリの番号は、ワークフローの中で github.repository_owner_id と github.repository_id として参照できます。条件を書く前に、実際の名乗りを一度確かめるのが確実です。
ほかにもあった落とし穴
| 起きたこと | 対処 |
|---|---|
| IAM ロールの説明文(Description)に日本語を入れたら、検査(cfn-lint)で止まった | 説明文は英語にした |
| OIDC の受け口を「既にあれば作らない」と判定したら、2回目の実行で、スクリプト自身が1回目に作った受け口を CloudFormation が消してしまった | 「既にあるか」ではなく「このスタックの中にあるか」で判定するようにした |
| 本番の Environment をタグからだけに限ったら、main から動いたジョブが、本番の手順に入る前に失敗扱いになった | ジョブに条件(if)を付け、main のときは本番のジョブを動かさないようにした(下のサンプルは、1つのジョブで行き先の Environment を選ぶ形なので、この問題は起きません) |
反映のワークフロー
反映のワークフローの見本です。使うときは .github/workflows/deploy.yml に置きます。
# deploy.example.yml
# GitHub Actions から AWS へ反映する見本(使うときは .github/workflows/deploy.yml に置く)。
# 長く使えるアクセスキーは置かず、OIDC で AWS のロールに入る(第3回)。
# - main に取り込んだら確認用(staging)へ。v で始まるタグを人が作ったら本番(prod)へ
# - ロールの ARN・証明書の ARN・ドメインは、GitHub の Environment の変数(vars)に置く(秘密ではない)
name: deploy
on:
push:
branches: [main]
tags: ['v*']
permissions:
id-token: write # OIDC のトークンを受け取るのに必要
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: {python-version: '3.13'}
- run: pip install -e ".[dev]" && ruff check . && pytest -q
deploy:
needs: test
runs-on: ubuntu-latest
environment: ${{ startsWith(github.ref, 'refs/tags/v') && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: {python-version: '3.13'}
- run: pip install -r infra/requirements.txt
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ap-northeast-1
- name: CDK で反映
working-directory: infra
run: >
npx --yes aws-cdk@2 deploy --require-approval never
-c stage=${{ vars.STAGE }} -c domainName=${{ vars.DOMAIN_NAME }}
-c certificateArn=${{ vars.CERTIFICATE_ARN }} -c wafEnabled=${{ vars.WAF_ENABLED }}
-c alarmEmail=${{ vars.ALARM_EMAIL }}
permissions: id-token: write:OIDC のトークンを受け取るのに必要な設定です。これがないとトークンをもらえません。ほかのものへの書き込み権限が増えるわけではありませんenvironment::タグ(refs/tags/v…)のときはproduction、それ以外(main)はstagingの Environment を使いますrole-to-assumeなどの値は、GitHub の Environment の変数(vars)に置きます。ロールの ARN(=AWS のものを指す名前)や証明書の ARN は秘密ではないので、Secrets ではなく変数で十分ですinfra/requirements.txtと CDK のコード、WAF_ENABLED・ALARM_EMAILは、後の回(第4回・第8回)で作ります。それまでは、このワークフローは置かずに見本として持っておきます
GitHub の Environment は、リポジトリの「Settings」の「Environments」で作ります。production の「Deployment branches and tags」で、v で始まるタグだけを許すように設定します。
反映の流れ
| いつ | 何が起きるか |
|---|---|
| プルリクエストを出したとき | API のテスト・CDK のテスト・検査(当社の構成では、コンテナが起動するかも確かめる) |
| main に取り込んだとき | 確認用に反映する |
| 人が v タグを作ったとき | 同じコードを本番に反映する |
当社は1人で開発しているので、プルリクエストの承認(CODEOWNERS)は使えません(作る人と承認する人が同じになるため)。代わりに「main は検査の合格が必須で、行き先は確認用」「本番は人がタグを作ったときだけ」とし、AI にはタグを作らせない 決まりにしました。CLAUDE.md にも「本番への公開のタグを作らない・push しない」と書いています(CLAUDE.md の書き方は第6回で扱います)。
動作確認
見本の JSON が正しい形か確かめ、検査とテストが通るままであることを確認します。
python3 -m json.tool infra/oidc-trust.example.json > /dev/null && echo ok
python3 -m json.tool infra/deploy-role-policy.example.json > /dev/null && echo ok
ruff check .
pytest -q
AWS と GitHub がそろったら、次のことを確かめます。
- main に取り込むと、
stagingの Environment のジョブが AWS に入れる(「Configure AWS credentials」の手順が成功する) - 条件の
subを1文字変えると断られる(条件が効いていることの確認。確かめたら戻す) - main のジョブが本番のロールに入ろうとしても断られる
なお、連載のサンプルは AWS への反映までは確かめていません。当社の社内基盤は同じ仕組みで反映しています。
つまずきやすい点・セキュリティ上の注意
subの条件にワイルドカード(*)を広く使わない:repo:組織名/*のように書くと、組織のどのリポジトリからでも入れてしまいます- トークンそのものをログに出さない:名乗りを確かめるときは、トークンを分解して
subだけを表示します - AWS のアクセスキーを GitHub の Secrets に残さない:OIDC に切り替えたら、古いキーは AWS 側で無効にしてから削除します
発注者向けメモ
開発会社に「自動で反映する仕組み(CI/CD)」を頼むときは、次を確認しておくと安心です。
- 「GitHub(などの CI)に、AWS のアクセスキーを保存しますか。OIDC にできますか」
- 「本番に反映できるのは、誰の、どの操作ですか(タグを作る人・承認する人)」
- 「AI のコーディングツールを使う場合、AI が本番に反映できない作りになっていますか」
- 「反映用のロールには、どこまでの権限が付きますか」
アカウントが複数ある、承認の手順を入れる、反映の前に人の確認を挟む、といった条件があると、設定の工数が増えやすくなります。
まとめと次回予告
- GitHub Actions から AWS へは、OIDC で入ればアクセスキーを置かずに済む
- ロールの条件は Environment で絞り、本番の Environment はタグからだけにする。AI にはタグを作らせない
- 新しい GitHub のリポジトリでは、名乗り(sub)が
組織名@番号/リポジトリ名@番号の形。条件を書く前に実際の名乗りを確かめる
次回は、いよいよ API を ECS Fargate と ALB で動かします。NAT を置かずに費用を抑える構成と、証明書・DNS で実際につまずいた落とし穴を説明します。
この連載の記事一覧
この記事は連載「Claude CodeとAWSで作るAIエージェントの土台」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Claude CodeとAWSで作るAIエージェントの土台 第1回】なぜリポジトリをやめて、役割ごとの API にしたのか
- 【Claude CodeとAWSで作るAIエージェントの土台 第2回】AWS を4つのアカウントに分け、Google Workspace でログインする
- 【Claude CodeとAWSで作るAIエージェントの土台 第3回】GitHub Actions から AWS へ入る(OIDC)と、sub が番号付きになる落とし穴(この記事)
- 【Claude CodeとAWSで作るAIエージェントの土台 第4回】ECS Fargate と ALB を NAT なしで小さく始める:証明書・DNS の落とし穴
- 【Claude CodeとAWSで作るAIエージェントの土台 第5回】トークンで役割を判定し、Claude Code の環境に持たせる(Network secrets)
- 【Claude CodeとAWSで作るAIエージェントの土台 第6回】CLAUDE.md は入口だけにする:役割ごとの指示書を API で配る
- 【Claude CodeとAWSで作るAIエージェントの土台 第7回】既存のリポジトリから移す:秘密の値の検査と、WordPress への投稿の中継
- 【Claude CodeとAWSで作るAIエージェントの土台 第8回】守りと費用:401 の監視、WAF の回数制限、確認用はふだん止める










コメント
コメント一覧 (2件)
[…] […]
[…] […]