MENU

問い合わせ


    【Claude CodeとAWSで作るAIエージェントの土台 第3回】GitHub Actions から AWS へ入る(OIDC)と、sub が番号付きになる落とし穴

    前回は、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 の間では、次の流れになります。

    1. ジョブが GitHub から OIDC のトークンを受け取る(中身に「どのリポジトリ・どの Environment・どのブランチやタグか」が書いてある)
    2. ジョブがそのトークンを AWS に見せ、ロール(=権限のまとまりを持った「役」)に入りたいと頼む
    3. 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 typeOpenID Connect
    Provider URLhttps://token.actions.githubusercontent.com
    Audiencests.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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      コメント

      目次