🔐

Terraform + GitHub OIDC で AWS デプロイの認証を安全に構築する

に公開

はじめに

GitHub Actions から AWS にデプロイするとき、最初に直面するのが「認証をどうするか」という問題です。

IAM ユーザーのアクセスキーを GitHub Secrets に入れる方法はシンプルですが、長期的なクレデンシャルをリポジトリに預けるリスクがあります。キーのローテーション運用も地味に面倒です。

GitHub OIDC(OpenID Connect)を使えば、長期クレデンシャルなしで AWS にアクセスできます。さらに Terraform でその構成を管理すれば、設定の再現性と可視性も確保できます。

この記事では、個人開発の日記アプリ Storyie で実際に構築した GitHub OIDC × Terraform の設計を紹介します。

なぜ OIDC なのか

従来の方法と OIDC の違いを整理します。

IAM ユーザー + アクセスキー方式

  • GitHub Secrets に AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY を登録
  • キーが漏洩すると永続的にアクセス可能
  • 定期的なローテーションが必要(でも忘れがち)

GitHub OIDC 方式

  • GitHub が発行する短命トークンで AWS に認証
  • 長期クレデンシャルが存在しない
  • ブランチやリポジトリ単位でアクセスを制限できる
  • AWS 側の信頼ポリシーで「どのリポジトリのどのブランチからのリクエストか」を検証

個人開発でもセキュリティは手を抜きたくない。OIDC なら設定コストもそこまで高くなく、一度構築すればローテーション運用から解放されます。

全体像

構成はシンプルです。

terraform/github-oidc/
├── main.tf                    # OIDC プロバイダー + IAM ロール + ポリシー
├── variables.tf               # 変数定義
├── outputs.tf                 # 出力(ロール ARN など)
└── terraform.tfvars.example   # 設定例

Terraform で管理するリソースは 3 つ:

  1. OIDC プロバイダー — GitHub Actions を AWS の信頼済み ID プロバイダーとして登録
  2. IAM ロール — GitHub Actions が assume する対象。信頼ポリシーでリポジトリ・ブランチを制限
  3. IAM ポリシー — SST デプロイに必要な AWS 権限をまとめたもの

Terraform の設計

OIDC プロバイダー

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

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
  ]
}

client_id_list には sts.amazonaws.com を指定します。これは GitHub Actions が AWS STS に対してトークン交換を行う際の audience です。

thumbprint_list は GitHub の OIDC エンドポイントの TLS 証明書のフィンガープリントです。AWS が GitHub からのトークンを検証するために使います。

信頼ポリシー — どのリポジトリ・ブランチを許可するか

data "aws_iam_policy_document" "github_actions_assume_role" {
  statement {
    effect = "Allow"

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

    actions = ["sts:AssumeRoleWithWebIdentity"]

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

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        for branch in var.github_branches :
        "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/${branch}"
      ]
    }
  }
}

ここがセキュリティの核心です。sub クレームの条件で、特定のリポジトリの特定のブランチからのリクエストだけを許可しています。

Storyie では maindevelop ブランチのみを許可しています。フィーチャーブランチからの直接デプロイは信頼ポリシーで弾かれるため、デプロイフローを強制できます。

変数で管理しているので、ブランチの追加・変更も terraform.tfvars を編集して apply するだけです。

variable "github_branches" {
  description = "List of GitHub branches allowed to assume the role"
  type        = list(string)
  default     = ["develop", "main"]
}

SST デプロイ用の IAM ポリシー

SST(Serverless Stack)は内部で CloudFormation、S3、Lambda、CloudFront など多くの AWS サービスを操作します。必要な権限をポリシードキュメントにまとめています。

data "aws_iam_policy_document" "sst_deploy" {
  # CloudFormation — SST のスタック管理
  statement {
    sid     = "CloudFormation"
    effect  = "Allow"
    actions = ["cloudformation:*"]
    resources = ["*"]
  }

  # S3 — アセット配信・SST state 管理
  statement {
    sid     = "S3"
    effect  = "Allow"
    actions = ["s3:*"]
    resources = ["*"]
  }

  # Lambda — Next.js の Server Functions
  statement {
    sid     = "Lambda"
    effect  = "Allow"
    actions = ["lambda:*"]
    resources = ["*"]
  }

  # CloudFront — CDN 配信
  # Route53 — DNS 管理
  # ACM — SSL 証明書
  # SQS + DynamoDB — Next.js ISR の Revalidation
  # ... 他のサービスも同様
}

正直、* を多用しているのは個人開発のトレードオフです。企業のプロジェクトであれば、リソース ARN を絞ってより厳格にすべきです。ただし個人開発では、SST がバージョンアップのたびに新しいリソースを作ることがあり、権限不足でデプロイが失敗するストレスとのバランスを取っています。

GitHub Actions 側の設定

Terraform で作成したロールを GitHub Actions で使います。

jobs:
  deploy:
    runs-on: self-hosted
    permissions:
      id-token: write   # OIDC トークンの取得に必要
      contents: read

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - name: Deploy to AWS with SST
        run: pnpm sst:deploy:prod

ポイントは 2 つです。

permissions.id-token: write: これがないと GitHub は OIDC トークンを発行しません。デフォルトでは id-token の権限は付与されていないので、明示的に書く必要があります。

aws-actions/configure-aws-credentials@v4: role-to-assume にロール ARN を渡すだけで、内部的に OIDC トークンの取得 → STS への AssumeRoleWithWebIdentity → 一時クレデンシャルの設定をすべてやってくれます。

AWS_ROLE_ARN は Terraform の output に表示されるので、それを GitHub Secrets に登録します。

セットアップ手順

1. Terraform の実行

cd terraform/github-oidc
cp terraform.tfvars.example terraform.tfvars
# terraform.tfvars を編集(github_org, github_repo を設定)
terraform init
terraform plan
terraform apply

2. GitHub Secrets の設定

terraform apply の output に表示されるロール ARN を GitHub リポジトリの Secrets に AWS_ROLE_ARN として登録します。

Terraform の outputs にはセットアップ手順も含めています。

output "github_secret_instructions" {
  value = <<-EOT
    1. Copy the role ARN above
    2. Go to Settings → Secrets and variables → Actions
    3. Name: AWS_ROLE_ARN
    4. Value: (ロール ARN)
  EOT
}

チームメンバーや将来の自分が迷わないよう、手順を output に埋め込んでおくのは地味に便利です。

3. ワークフローの実行

あとは maindevelop にプッシュすれば、OIDC 認証経由で自動デプロイが走ります。

state 管理について

この構成では Terraform の state をローカルに保存しています。

backend "local" {
  path = "terraform.tfstate"
}

OIDC の設定は一度作ったらほぼ変更しないため、S3 + DynamoDB のリモートバックエンドを組むほどではないと判断しました。個人開発で触るのは自分だけなので、state の競合も起きません。

ただし、terraform.tfstate.gitignore に入れるのを忘れずに。state には AWS アカウント ID やロール ARN が含まれます。

実際に運用してみて

良かった点

  • ローテーション運用ゼロ: 一度構築したら完全に忘れていい。これが一番大きい
  • ブランチ制限が自然なガードレール: フィーチャーブランチから誤ってデプロイすることがなくなった
  • Terraform で構成が可視化される: 半年後に「このロールの権限なんだっけ」と思っても .tf ファイルを見れば分かる
  • セットアップ手順が output に残る: 新しいリポジトリで同じ構成を作るときに迷わない

注意した点

  • id-token: write の指定忘れ: 最初にハマったポイント。OIDC トークンが取得できず AccessDenied になる
  • thumbprint の更新: GitHub が証明書を更新すると thumbprint が変わる可能性がある。エラーが出たら確認する
  • self-hosted runner との組み合わせ: Storyie では self-hosted runner を使っているが、OIDC 自体は GitHub-hosted でも self-hosted でも同じように動く

まとめ

GitHub OIDC + Terraform の組み合わせは、個人開発でも十分にペイする投資です。

  • 長期クレデンシャルの管理から解放される
  • ブランチレベルでデプロイ権限を制御できる
  • Terraform で構成が再現可能になる

セキュリティを「面倒だから後回し」にしがちな個人開発だからこそ、最初に仕組みで解決しておくと安心してコードに集中できます。


この記事で紹介した構成は、日記アプリ Storyie の実際のデプロイ基盤です。

📝 Web: https://storyie.com
🍎 iOS: App Store
🤖 Android Beta: https://storyie.com/android-beta

Discussion