本文へスキップ
hdknr blog
戻る

キーを持たずに AWS へデプロイし、夜間に自動適用する — GitHub Actions 運用設計ノート

サーバーレス前提の自動配信システムを題材に、「GitHub Actions から AWS へ安全にデプロイする」仕組みを、つまずきやすいポイントごとに解説します。キーレス認証(OIDC)と、稼働中システムを壊さないための「夜間デプロイ」設計が主題です。

対象は AWS + Terraform/OpenTofu + GitHub Actions でデプロイを回している(あるいはこれから組む)チームです。なお、本記事では実アカウント ID やリポジトリ名は伏せ字(<ACCOUNT_ID> など)にしています。

この記事で扱うこと

1. なぜ「AWS キー」を置かなくてもデプロイできるのか

GitHub Actions から AWS を操作する、と聞くと多くの人がまず「AWS のアクセスキーを GitHub の Secrets に登録しているんでしょ?」と考えます。今回の構成では登録していません。 長期のアクセスキー/シークレットキーはどこにも保管していないのです。

代わりに使うのが GitHub OIDC(OpenID Connect)フェデレーション。実行のたびに発行される、数十分で失効する一時クレデンシャルを使います。

流れ

全体像は下図のとおりです。GitHub が発行する短命の署名トークン(id-token)を AWS STS が検証し、デプロイ用ロールの一時クレデンシャルに引き換えます。

GitHub Actions の job が発行する id-token を configure-aws-credentials が AWS STS に提示し、STS が IAM の OIDC IdP と信頼ポリシー条件を検証して数十分で失効する一時クレデンシャルを発行、tofu apply がそれで AWS を操作するキーレスデプロイの流れ図

  1. job 側で permissions: id-token: write を付与すると、GitHub が「この run は repo:<ORG>/<REPO> で動いている」と署名した短命の JWT(id-token)を発行する
  2. aws-actions/configure-aws-credentials がその JWT を AWS STS に提示し、sts:AssumeRoleWithWebIdentity を呼ぶ
  3. AWS STS が IAM の OIDC IdP とロールの信頼ポリシー条件を検証し、デプロイ用ロールの「一時クレデンシャル(数十分で失効)」を発行する
  4. tofu apply がその一時クレデンシャルで AWS を操作する

ワークフロー側はこれだけ

permissions:
  id-token: write     # ← OIDC トークンを取得する許可(キーレスの鍵)
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v6
    with:
      role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/<deploy-role>
      aws-region: ap-northeast-1

role-to-assume に書くのはロールの ARN という公開情報であって、クレデンシャルではありません。秘密の文字列を一切置かないのがポイントです。

AWS 側の「信頼ポリシー」

AWS 側には、GitHub の OIDC プロバイダ(https://token.actions.githubusercontent.com)を IAM に登録し、デプロイ用ロールの信頼ポリシーで「どの GitHub から来たトークンを信じるか」を条件で縛ります。

条件 (claim)意味
audsts.amazonaws.comAWS STS 向けのトークンに限定
sub(前方一致)repo:<ORG>/<REPO>:*このリポジトリからの run のみ assume 可(branch / PR / tag は問わない)

つまり「AWS キーがあるから動く」のではなく、**「GitHub が “このリポの run だ” と OIDC で証明 → AWS がその証明を信頼して一時クレデンシャルを貸す」**という信頼関係を作り込んでいるわけです。

キー方式と比べて何が嬉しいか

観点長期アクセスキーOIDC(採用)
漏洩リスクSecrets に保管。漏れたら悪用される保管する秘密がない
失効手動ローテーションが必要数十分で自動失効
利用範囲キーを持てば誰でもsub 条件で特定リポのみ

2. 鶏と卵問題 —「最初の 1 回」だけは手動

ここで素朴な疑問が出ます。

その「デプロイ用ロール」も Terraform で作るんでしょ? じゃあ最初は誰が作るの?

鋭い。GitHub Actions はそのロールを assume して動くのに、ロールがまだ無い初回は assume する先が存在しない — これが鶏と卵問題です。

解決策はシンプルで、OIDC の信頼関係を最初に一度だけ作る apply は、人間が自分の AWS 管理者クレデンシャルでローカル実行する。このために、信頼関係や state 保管用バケットを作る部分を bootstrap という別スタックに分離しておきます。

cd infra/bootstrap
mv backend.tf backend.tf.disabled   # 初回は手元のローカル state で
tofu init
tofu apply                          # ← ここで OIDC IdP + デプロイ用ロール + state バケットが出来る
mv backend.tf.disabled backend.tf
tofu init -migrate-state            # state を S3 へ引っ越し

この 1 回が済めば信頼関係が AWS 上に存在するので、以降は手動クレデンシャルを一切使わず、GitHub Actions が OIDC で回します。手動 apply が再び必要になるのは「新しい AWS アカウントへ移設する」「state バケットを作り直す」といった初期化のときだけです。

3. 「マージしたら即デプロイ」を見直した理由

最初の構成は、よくある形でした。

on:
  push:
    branches: [main]
    paths: ['infra/**']

maininfra/** の変更が入ると即 tofu apply。手軽ですが、稼働中システムではこんな不安が出ます。

デプロイで関数が入れ替わる瞬間に、ユーザーがフォーム送信したり、メッセージング連携の友だち追加が走ったらどうなる?

まず安心な事実:1 リクエストは「割れない」

サーバーレス関数のコード差し替えは呼び出し単位でアトミックです。

なので「フォーム送信が中途半端に処理される」は構造上起きません。

でも、1 つだけ弱い経路があった

我々のシステムには、友だち追加に対して同期的に・数秒以内に応答メッセージを返す経路がありました。これは入れ替え時のコールドスタートに最も弱い。しかも失敗してもプラットフォーム側の再送は、重複防止のため仕様上抑止されます。つまり取りこぼすとリトライが効かない唯一の経路だったのです。

ここから出した結論:

デフォルトのデプロイは、こうした流入が少ない深夜に倒したい。 ただし、すぐ反映して動作確認したい変更は即時でやりたい。

4. 解決策:即時 / 夜間を「都度選べる」ようにする

push トリガーをやめて、起動経路を 2 つに分けました。

on:
  schedule:
    - cron: '0 17 * * *'   # UTC 17:00 = 02:00 JST(夜間バッチ)
  workflow_dispatch:        # = 即時デプロイ(手動 run)
経路起動方法いつ使う
即時gh workflow run deploy.yml(または画面の Run workflow)マージ後すぐ反映して検証したい変更
夜間何もしない(毎日 02:00 JST の cron が main の現状を適用)急がない変更(デフォルト)

この設計の良いところ:

トレードオフは「即時がワンクリックでなくなる」ことだけ。merge = deploy の手軽さは捨てましたが、安全側のデフォルトを得ました。

5. 無人で apply するなら、plan は事前に人が見ておく

夜間 cron は tofu apply -auto-approve無人で走らせます。ここで怖いのは、replacedestroy を含む「驚きのある変更」が、誰のレビューも経ずに深夜に適用されてしまうこと。

そこで前提を 1 つ足しました。

apply は「PR の時点でレビュー済みの plan を適用するだけ」にする。

PR を開くと、pull_request トリガーで tofu plan だけを実行し(apply はしない)、結果を PR にコメントするワークフローを追加しました。レビュアーはマージ前にこの plan を見て、replace/destroy が無いかを確認します。

on:
  pull_request:
    paths: ['infra/**']
# tofu plan -lock=false -no-color の結果を PR コメントに投稿(apply はしない)

これで「夜間の無人 apply = 確認済み変更の適用」となり、安心して放置できます。

おまけ:永続ドリフトは潰しておく

この plan チェックを入れて初めて気づいたのですが、ある SAML 連携リソースが毎回 “1 to change” を出す永続ドリフトを起こしていました。メタデータ URL からサーバー側が派生フィールドを自動展開します。設定ファイルには書いていないため、毎回「消そうとしては再展開される」無限ループになっていました。

無人 apply 時代には、こういう「毎晩出続ける無害な差分」が本物のドリフトを覆い隠す(レビュー疲れ)ので、lifecycle { ignore_changes = [...] } で黙らせました。**「No changes が正常」**という状態を保つのが、無人運用の前提です。

6. 最後に残る制約:Terraform は「今の main 全体」を適用する

1 つだけ、運用で守るルールがあります。

tofu apply は常に 「その時点の main の全状態」を適用する。 したがって、夜間に回したい変更が main に乗ったまま別の変更を即時 dispatch すると、延期中の変更も巻き込んで昼間に反映されてしまう。

完全な「PR 単位の分離」は Terraform の性質上できません。対策は運用で、

幸い、配信系には冪等性の安全装置(決定的なスケジュール名による重複防止、再送の skip、検索優先のレコード名寄せ)が効いているので、取りこぼしてもユーザーの再操作で安全に収束します。無人運用は「冪等性」と「クリーンな plan」の上に成り立つ、というのが学びでした。

まとめ

テーマ結論
認証OIDC でキーレス。秘密は保管せず、一時クレデンシャルを毎回発行
初期化信頼関係を作る「最初の 1 回」だけ手動(鶏と卵)
起動push 即デプロイをやめ、即時(dispatch) / 夜間(cron) を選択制に
既定デフォルト夜間で、入れ替えに弱い経路への曝露を最小化
安全plan は PR で人が確認 → 夜間は確認済み変更の無人適用
前提No changes を保つ(ドリフト除去)+ 1 本ずつマージ

「キーを置かない」「即デプロイをやめる」――どちらも一見すると手間が増える選択ですが、稼働中システムを安全に回すための、地に足のついたトレードオフでした。



前の記事
寝ている間に仕事を自動化する2026年の GitHub リポジトリ 10 選 — 全部無料・オープンソース
次の記事
Claude Code は『賢いが健忘症のプログラマ』— Trellis でプロジェクトの脳を持たせる