GitHub Actions OIDC × Environments で複数 AWS アカウントへのデプロイを    1 つの workflow にまとめてみた

GitHub Actions OIDC × Environments で複数 AWS アカウントへのデプロイを 1 つの workflow にまとめてみた

2026.05.25

GitHub Actions の OIDC で複数 AWS アカウントにデプロイする workflow を、GitHub Environments でまとめてみました。

prod / stg のように AWS アカウントが分かれている構成では、GitHub Actions 側で IAM Role の ARN をどう切り替えるかが論点になります。今回は GitHub Environments の Environment-scoped Variables に ARN を持たせて workflow を1本にまとめ、各アカウントの Trust Policy では sub claim を environment 単位に制限しました。Environment の取り違えがあっても、別アカウントの IAM Role は OIDC 認証で弾かれます。

やりたいこと

1つの deploy.yml から workflow_dispatch の入力で prod / stg を選び、対応する AWS アカウントの IAM Role を AssumeRole します。

ポイントは次の2つです。

  • GitHub 側: Environment-scoped Variables に IAM Role ARN を持たせて workflow を1本にまとめる
  • AWS 側: 各アカウントの Trust Policy で environment・ブランチ・リポジトリを condition で絞り、別環境や別ブランチの OIDC token では AssumeRole できない状態にする

workflow YAML

検証用に以下のworkflowファイルを用意しました。

.github/workflows/deploy.yml
name: deploy

on:
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options:
          - prod
          - stg

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v6
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - run: aws sts get-caller-identity

environment: ${{ inputs.environment }} で Environment を切り替え、ARN は vars.AWS_ROLE_ARN で参照します。Environment ごとに値が変わるので、workflow 側では同じ式を書くだけで済みます。

実際に environment=prodenvironment=stg で実行すると、aws sts get-caller-identity の出力が切り替わります。

// environment=prod の出力
{
  "Account": "111111111111",
  "Arn": "arn:aws:sts::111111111111:assumed-role/GHADeployRole/GitHubActions"
}

// environment=stg の出力
{
  "Account": "222222222222",
  "Arn": "arn:aws:sts::222222222222:assumed-role/GHADeployRole/GitHubActions"
}

GitHub 側の設定

Environments を作る

Settings → Environments → New environmentprodstg を作成します。

Environment ごとに Variables を入れる

各 Environment の Environment variablesAWS_ROLE_ARN を登録します。

Environment Variable 名
prod AWS_ROLE_ARN arn:aws:iam::111111111111:role/GHADeployRole
stg AWS_ROLE_ARN arn:aws:iam::222222222222:role/GHADeployRole

ARN は秘匿情報ではないので Secrets ではなく Variables で十分です。Secrets にすると GitHub Actions のジョブログ側で *** に置き換わります。CloudTrail のイベントと突き合わせて切り分けたいときに、GHA ログから ARN を直接読めない方が面倒なので Variables にしておくのが楽です。

Deployment branches and tags

Settings → Environments → <env> → Deployment branches and tags で許可ブランチを設定します。

  • prod: main のみ
  • stg: mainfeature/*

Configure_environment_prod_·_…_gha-oidc-environments-sample.png

Configure_environment_stg_·_ms…_gha-oidc-environments-sample.png

ここで設定したブランチ制御は GitHub 側でジョブ起動前にブロックする層です。AWS 側でも Trust Policy の condition で ref 等を制限できるので、両方書いて二重防御にしておくのがおすすめです(後述の「ブランチ単位でも制限する」節で詳しく説明します)。

AWS 側の設定(Terraform)

各 AWS アカウントに必要なのは次の2つです。

  • OIDC Provider(token.actions.githubusercontent.com
  • IAM Role(Trust Policy の sub claim を repo:OWNER/REPO:environment:NAME で制限する)

ここでは1ルート構成 + partial backend config + env ごとの tfvars で同じコードを使い回します。

ディレクトリ構成

.
├── versions.tf
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── envs/
    ├── prod.tfbackend
    ├── prod.tfvars
    ├── stg.tfbackend
    └── stg.tfvars

versions.tf

backend は空宣言(partial config)にして、path は init 時に注入します。

versions.tf
terraform {
  required_version = ">= 1.6"

  backend "local" {}

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

main.tf

OIDC Provider は data source で参照します。GitHub Actions 用の OIDC Provider はアカウントに1つだけ作る共有リソースとして扱う想定です。sub claim の environment は変数から組み立てます。

main.tf
provider "aws" {
  region = var.aws_region
}

data "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"
}

data "aws_iam_policy_document" "trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [data.aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:${var.repository}:environment:${var.environment_name}"]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name               = var.role_name
  assume_role_policy = data.aws_iam_policy_document.trust.json
}

OIDC Provider をまだ作っていないアカウントでは、初回だけ別途作る必要があります。マネジメントコンソールでも作れますし、別の Terraform 設定で1回作って終わり、でも良いです。

variables.tf

variables.tf
variable "environment_name" {
  type = string
}

variable "repository" {
  type = string
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "role_name" {
  type    = string
  default = "GHADeployRole"
}

outputs.tf

outputs.tf
output "role_arn" {
  value = aws_iam_role.github_actions.arn
}

環境別の backend config

state ファイルのパスを env ごとに分けます。

envs/prod.tfbackend
path = "terraform.prod.tfstate"
envs/stg.tfbackend
path = "terraform.stg.tfstate"

共通の tfvars

repo 固有の値は terraform.tfvars に置きます。terraform.tfvars は自動ロードされるので、apply 時に指定しなくても読み込まれます。

terraform.tfvars
repository = "ORG/REPO"

role_name は env で変わらない想定なので variables.tf の default をそのまま使います。変えたい場合だけ terraform.tfvars 側に追記すれば足ります。

環境別の tfvars

env で値が変わるのは environment_name だけです。

envs/prod.tfvars
environment_name = "prod"
envs/stg.tfvars
environment_name = "stg"

apply 手順

env を切り替えるときは terraform init -backend-config=... -reconfigure で backend を入れ替えてから apply します。AWS の認証情報も env ごとに切り替えます(AWS_PROFILE を変えるか、別シェルで作業)。

# prod アカウントの認証情報を有効化(profile や環境変数で)
terraform init -backend-config=envs/prod.tfbackend -reconfigure
terraform apply -var-file=envs/prod.tfvars

# stg アカウントの認証情報に切り替えてから
terraform init -backend-config=envs/stg.tfbackend -reconfigure
terraform apply -var-file=envs/stg.tfvars

作業ディレクトリに terraform.prod.tfstateterraform.stg.tfstate が並んで作られます。apply 後に出力される role_arn を、対応する GitHub Environment の AWS_ROLE_ARN Variable にコピーすれば完成です。

生成される Trust Policy

参考までに、Terraform が生成する prod アカウントの IAM Role の Trust Policy はこんな形になります。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::111111111111: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:ORG/REPO:environment:prod"
      }
    }
  }]
}

stg アカウントの IAM Role は subrepo:ORG/REPO:environment:stg になります。

これで「prod アカウントの Role は prod Environment の OIDC token しか受け付けない」状態になります。Environment の取り違えで別アカウントの Role を叩こうとしても OIDC 認証で弾かれます。

実際に試したところ、prod の Variable AWS_ROLE_ARN を stg のロール ARN に差し替えて environment=prod で実行すると Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity で失敗しました。stg アカウントの CloudTrail にも errorCode: AccessDenied のイベントが残ります。後述の「検証時に役立つツール」節で具体例を載せます。

ここまでは sub claim 1 つだけの最小形です。ブランチや repository でも制限したい場合は、後述の「ブランチ単位でも制限する」節で condition を足す例を載せます。

sub claim の挙動

GitHub 公式ドキュメントには次のように書かれています。

If you use a workflow with an environment, the sub field must reference the environment name:
repo:ORG-NAME/REPO-NAME:environment:ENVIRONMENT-NAME

引用元: Configuring OpenID Connect in Amazon Web Services - Configuring the role and trust policy

environment: を指定すると sub claim は ref 形式から environment 形式に切り替わります。両方は入らない仕様です。

ブランチ単位でも制限する(Trust Policy と Deployment branches の二重防御)

Environment 単位で sub を絞れば AWS アカウントの取り違えは防げますが、それだけだと「同じ Environment の中で別ブランチから OIDC token が発行されたケース」は素通りします。ブランチも縛りたい場合、AWS 側と GitHub 側の両方で防御層を作れます。

AWS IAM 側で refrepository を condition に書く

AWS は 2026-01 のアップデートで、Trust Policy の Condition に GitHub OIDC のプロバイダー固有 claim を書けるようにしました。それまでは aud / sub / email / oaud / amr の5つしか評価対象でなかったところに、下記の claim が加わっています。

AWS STS condition key 対応する JWT claim
token.actions.githubusercontent.com:actor actor
token.actions.githubusercontent.com:actor_id actor_id
token.actions.githubusercontent.com:job_workflow_ref job_workflow_ref
token.actions.githubusercontent.com:repository repository
token.actions.githubusercontent.com:repository_id repository_id
token.actions.githubusercontent.com:workflow workflow
token.actions.githubusercontent.com:ref ref
token.actions.githubusercontent.com:environment environment
token.actions.githubusercontent.com:enterprise_id enterprise_id

参考:

sub の評価は引き続き必須なので、これらは sub に重ねて condition を増やす形になります。

prod 用 Role の Trust Policy に refrepository を重ねるとこうなります。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::111111111111: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:ORG/REPO:environment:prod",
        "token.actions.githubusercontent.com:repository": "ORG/REPO",
        "token.actions.githubusercontent.com:ref": "refs/heads/main"
      }
    }
  }]
}

これで prod アカウントの Role は「ORG/REPO の main ブランチから、prod Environment を指定して動いたジョブ」しか引き受けません。

実機で :ref の値だけ refs/heads/nonexistent-test-branch に差し替えて、main から environment=stg を実行してみると Not authorized to perform sts:AssumeRoleWithWebIdentity で弾かれます。ref が condition として評価されている裏付けです。

stg 側で feature/* も許可したい場合、StringEquals ではブランチ名のワイルドカードが書けないので StringLike にして配列で渡します。

"StringLike": {
  "token.actions.githubusercontent.com:ref": [
    "refs/heads/main",
    "refs/heads/feature/*"
  ]
}

GitHub の Deployment branches and tags で UI 側でも制限する

Settings → Environments → <env> → Deployment branches and tags でも、どのブランチ・タグからその Environment にデプロイできるかを設定できます。

選べる選択肢は次の3つです。

  • Selected branches and tags: 個別にブランチ・タグを allowlist 登録
  • Protected branches only: branch protection rules が設定されたブランチのみ許可
  • All branches: 制限なし

例として今回の構成では次のように設定しています。

  • prod Environment → main のみ許可
  • stg Environment → mainfeature/* を許可

許可外ブランチで Environment を指定したジョブを実行すると、Actions 側でジョブが起動せず数秒で failure 扱いで終わります。実機で feature/test-branch から environment=prod を実行したケースでは、checkout 以降のステップは一切走りませんでした。AWS まで OIDC リクエストが届かないので、Trust Policy 側の condition チェックは発動しません。

deploy_·_msato0731_gha-oidc-environments-sample_14bb03c.png

二重防御として併用する

Trust Policy と Deployment branches はそれぞれ防御層が違うので、両方書いておくと役割が明確になります。

役割 失敗時の挙動
Deployment branches and tags(GitHub 側) 許可外ブランチからのジョブを起動前に弾く Actions ジョブが起動せずに failure
Trust Policy(AWS 側) OIDC token の中身を最終チェック AssumeRole で AccessDenied

検証時に役立つツール

OIDC 連携の動作確認は GitHub 側と AWS 側の両方から取れます。

GitHub Actions のログで OIDC token の claim を確認する

steve-todorov/oidc-debugger-action を workflow に一時的に入れると、Actions のログに JWT を decode した結果(sub / aud / repository / ref など約30個の claim)が JSON で出力されます。

- uses: steve-todorov/oidc-debugger-action@b6d83b24a379e77233fb6b26f1c1a662c4bf443c  # v1.0.2
  with:
    audience: sts.amazonaws.com

実際の出力(抜粋)です。

{
  "sub": "repo:ORG/REPO:environment:prod",
  "aud": "sts.amazonaws.com",
  "environment": "prod",
  "ref": "refs/heads/main",
  "ref_type": "branch",
  "repository": "ORG/REPO",
  "actor": "USERNAME",
  "workflow_ref": "ORG/REPO/.github/workflows/deploy.yml@refs/heads/main"
}

sub claim が repo:ORG/REPO:environment:prod の形になっているか、ここで一発で確認できます。Trust Policy の condition がハマっているときの一次切り分けに便利です。

JWT に含まれている ref / repository / actor などの claim も、AWS IAM Trust Policy 側の condition key として書けば評価されます(前節参照)。Trust Policy の condition を増やすときは、まずここで実値を確認してから書くと安心です。

サードパーティの個人リポジトリ依存になる点は注意してください。使う場合はコミット SHA でピン留めしておくと、後からタグが別の commit に張り替えられても影響を受けません。

AWS 側で実際に渡された claim を確認する

CloudTrail の AssumeRoleWithWebIdentity イベントを開くと、userIdentity フィールドに GitHub から渡された subject が記録されています。requestParameters には roleArn / roleSessionName / durationSeconds のみが入る形式で、subject 自体は userIdentity.userNameuserIdentity.principalId の末尾で確認できます。Trust Policy の Condition でブロックされたケースでも、どの subject で来たかを後追いできます。

実際のイベント例です(別環境の token で拒否されたケース)。

{
  "eventName": "AssumeRoleWithWebIdentity",
  "errorCode": "AccessDenied",
  "errorMessage": "An unknown error occurred",
  "userIdentity": {
    "type": "WebIdentityUser",
    "userName": "repo:ORG/REPO:environment:prod",
    "principalId": "arn:aws:iam::222222222222:oidc-provider/token.actions.githubusercontent.com:sts.amazonaws.com:repo:ORG/REPO:environment:prod",
    "identityProvider": "arn:aws:iam::222222222222:oidc-provider/token.actions.githubusercontent.com"
  },
  "requestParameters": {
    "roleArn": "arn:aws:iam::222222222222:role/GHADeployRole",
    "roleSessionName": "GitHubActions",
    "durationSeconds": 3600
  }
}

このケースでは sub が :environment:prod ですが、roleArn は stg アカウントのロール(Trust Policy は :environment:stg を要求)だったため AccessDenied で拒否さています。

おわりに

GitHub Environments を軸に、workflow を1本のまま複数 AWS アカウントへデプロイする構成を試しました。アカウントが増えても workflow を増やす必要がなく、Environment と Trust Policy を1セット足せば追従できます。(今回はFreeプランのPrivateリポジトリで検証しました)

ブランチ単位の制限は AWS IAM 側の 2026-01 のアップデートで Trust Policy にも書けるようになりました。GitHub の Deployment branches and tags と組み合わせて、UI 側で起動前に弾く層と、AWS 側で OIDC token を最終チェックする層を二重で持っておくのが今のおすすめです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事