こういった課題は、Terraformを個人で使い始めた段階から、チームに広げようとした瞬間に一気に顕在化します。コードレビューはできても、そのコードを適用したらインフラがどう変わるのかをレビュアーが把握できていない、というのは非常に多い現場の悩みです。そこへ「誰かがローカルから手動applyした」という事故が重なると、tfstateが壊れてインフラの実態とコードの乖離が始まります。
この記事では、GitHub Actionsを使ってTerraformのCI/CDパイプラインを構築する方法を解説します。PRトリガーによるterraform plan自動実行とPRへの結果コメント投稿、GitHub Environmentsを使った本番applyの承認フロー、AWSへのOIDC認証設定まで、チーム開発で実際に使えるワークフロー設計の全体像をカバーします。
動作確認環境: GitHub Actions(ubuntu-latestランナー)、Terraform 1.8.5、AWS(ap-northeast-1リージョン)。
この記事のポイント
・PR作成時にterraform planを自動実行してレビュアーが差分を確認できる
・GitHub Environmentsで本番applyに承認フローを挟み誤操作を防ぐ
・OIDCを使いアクセスキー不要でAWSへ安全に認証する方法
・workflow_dispatchで緊急時の手動apply実行を安全に制御する方法
・if: failure()でapply失敗時のSlack通知を組み込みパイプライン障害を見逃さない
・initの失敗・plan差分なし・OIDC認証エラー・stateロック残留の対処法
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜTerraformにCI/CDが必要なのか
Terraformを使いはじめた段階では、「ローカルでplan→目視確認→apply」という運用で十分に機能します。しかし、チームで作業するようになると、このワークフローは急速に機能しなくなります。1. ローカルplanの運用が抱える問題
ローカルでterraform planを実行しても、その結果はターミナルに表示されるだけです。コードレビューでPull Requestを見ているレビュアーは、コードの差分を見ているに過ぎず、「このコードを実際に適用するとインフラがどう変わるのか」を目にすることができません。また、複数のエンジニアが同時にローカルからapplyを実行しようとすると、tfstateのロック競合が発生します。S3バックエンドとDynamoDBによるstateロックを設定していても、applyのタイミングをチームで調整する手間は残ります。
Terraformのバージョンがエンジニアごとにバラバラになるケースも見落とされがちです。ローカルで `terraform 1.7.0` を使っているエンジニアと `1.8.5` を使っているエンジニアが混在していると、plan結果に差異が出ることがあります。CI/CDで実行バージョンをコードで固定することで、この問題を根本から解消できます。
さらに、「誰がいつapplyしたか」の証跡をterraformコマンドのログとして残すのは困難です。GitHubのワークフロー経由でapplyを一元化すれば、変更の履歴とapplyのログが自然に残ります。「手動applyをやめて全員がGitHub Actionsを通して実行する」という一点だけでも、運用事故の件数は大幅に減ります。
2. CI/CDが解決する3つの課題
GitHub ActionsによるCI/CDパイプラインは、以下の3点を解決します。・planの可視化:PR作成・更新時に自動でterraform planを実行し、結果をPRコメントとして投稿します。レビュアーはコードの差分とplan結果を同じ画面で確認できます。
・applyの権限制御:applyはmainブランチへのマージ後、かつ承認者のレビューを通過した場合のみ実行されます。任意のタイミングで誰でもapplyできる状態を解消します。
・証跡の記録:applyはGitHub Actionsのジョブとして実行されるため、実行者・タイムスタンプ・ログがGitHubのWorkflow履歴として残ります。セキュリティ監査や障害対応のふりかえりにも使えます。
CI/CDの設計パターンを選ぶ
Terraform CI/CDの設計パターンは主に2つあります。チームの規模と環境の重要度によって選択します。1. パターンA(シンプル型):マージ後に自動apply
PRを作成するとterraform planが実行され、mainブランチへのマージをトリガーとしてterraform applyが自動実行されます。承認フローはなく、PR承認が実質的なapplyのゲートになります。・開発環境・ステージング環境に向いている
・セットアップが簡単で、CI/CDの導入初期に採用しやすい
・本番環境には向かない(マージ即applyのため誤操作リスクが残る)
2. パターンB(承認フロー型):GitHub Environmentsで承認後にapply
PR作成→planの確認→マージ、ここまでは同じです。その後、GitHub Environmentsの「Required reviewers」設定により、承認者がapplyを許可するまでワークフローが待機します。承認後にterraform applyが実行されます。・本番環境向き
・承認者を特定のユーザーに限定できる
・承認待ち状態がGitHubのUIで可視化される
・承認の有効期限(タイムアウト)も設定可能で、放置を防げる
この記事ではパターンBを中心に解説します。本番運用に耐えられる設計を最初から採用する方が、後から追加するよりもコストが低いためです。
なお、「planワークフローだけ先に入れる」という段階的な導入も有効です。まずPRコメントへのplan結果投稿だけを実現してチームに習慣づけ、次のステップでapplyワークフローとEnvironment承認フローを追加するという順序でCI/CDを整備できます。
>> Terraform実践セミナーの詳細はこちら
AWSへの認証設定(OIDCを使う)
GitHub ActionsからAWS APIを呼び出すにはIAM認証が必要です。かつてはアクセスキーをGitHub Secretsに登録する方法が主流でしたが、現在はOIDC(OpenID Connect)を使う方法が推奨されています。1. OIDCを使う理由
アクセスキーをGitHub Secretsに登録する方法は、キーが長期間有効であることが問題です。Secretsの設定ミスや流出事故があると、長期間にわたってAWS操作権限が悪用されるリスクがあります。OIDCでは、GitHub Actionsが実行されるたびに一時トークンをAWSから取得します。トークンの有効期間は通常1時間未満で、静的なアクセスキーを保持しません。長期的な認証情報の管理コストとリスクを根本から排除できます。実務上も「アクセスキーのローテーションを忘れていた」という運用事故がなくなるのは大きなメリットです。
2. IAM IDプロバイダーの登録
まず、GitHubをAWSのIAM IDプロバイダーとして登録します。# GitHubのOIDCプロバイダーURLとフィンガープリントを使って登録 aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com \ --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 # 作成されたARNを確認する aws iam list-open-id-connect-providers
3. IAMロールの作成(信頼ポリシーの設定)
次に、GitHub ActionsがAssumeRoleできるIAMロールを作成します。信頼ポリシーで、どのリポジトリのどのブランチからの実行を許可するかを厳密に指定します。# trust-policy.json の内容 # your-org/your-repo の部分を実際のリポジトリに変更する { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*" } } } ] } # IAMロールを作成 aws iam create-role \ --role-name TerraformCIRole \ --assume-role-policy-document file://trust-policy.json # Terraformに必要な権限ポリシーをアタッチ(例: AdministratorAccess) # 実際の運用では最小権限のカスタムポリシーを作成することを推奨 aws iam attach-role-policy \ --role-name TerraformCIRole \ --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
GitHub Actionsワークフローの実装
ワークフローは2つのファイルに分けます。plan用(PRトリガー)とapply用(mainマージ後トリガー)です。リポジトリの `.github/workflows/` ディレクトリに配置します。ワークフローファイルの配置構成
ワークフローを作成する前に、推奨するリポジトリ構成を確認しておきます。.github/workflows/配下にワークフローYAMLを2ファイル配置し、tfファイルはterraform/ディレクトリにまとめます。terraform-infra/ ├── .github/ │ └── workflows/ │ ├── terraform-plan.yml # PRトリガー: plan実行+PRコメント投稿 │ └── terraform-apply.yml # mainマージ後: apply実行(承認フロー付き) ├── terraform/ │ ├── main.tf # メインのリソース定義 │ ├── variables.tf # 変数定義 │ ├── outputs.tf # 出力値の定義 │ ├── provider.tf # AWS providerとbackend設定 │ └── environments/ │ ├── dev.tfvars # 開発環境の変数値 │ └── prod.tfvars # 本番環境の変数値 └── trust-policy.json # IAMロール信頼ポリシー(OIDC設定時に使用)
1. planワークフロー(PR作成・更新時)
# .github/workflows/terraform-plan.yml name: Terraform Plan on: pull_request: branches: - main paths: - '**.tf' - '**.tfvars' permissions: contents: read id-token: write # OIDCトークン取得に必要 pull-requests: write # PRコメントの投稿に必要 jobs: plan: name: Terraform Plan runs-on: ubuntu-latest defaults: run: working-directory: ./terraform # tfファイルのあるディレクトリ steps: - name: Checkout uses: actions/checkout@v4 - name: Configure AWS Credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/TerraformCIRole aws-region: ap-northeast-1 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.8.5 - name: Terraform Init id: init run: terraform init - name: Terraform Format Check id: fmt run: terraform fmt -check continue-on-error: true - name: Terraform Validate id: validate run: terraform validate - name: Terraform Plan id: plan run: terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt continue-on-error: true # planの結果をPRコメントに投稿する - name: Post Plan Result to PR uses: actions/github-script@v7 with: script: | const fs = require('fs'); const planOutput = fs.readFileSync('./terraform/plan_output.txt', 'utf8'); const truncated = planOutput.length > 60000 ? planOutput.substring(0, 60000) + '\n... (出力が長すぎるため省略)' : planOutput; const body = `## Terraform Plan 結果 #### Format: \`${{ steps.fmt.outcome }}\` #### Validate: \`${{ steps.validate.outcome }}\` #### Plan: \`${{ steps.plan.outcome }}\`
`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body });Plan の詳細を見る
\`\`\` ${truncated} \`\`\`
`continue-on-error: true` をplanステップに設定している理由は、planが失敗してもPRコメントにその失敗内容を投稿できるようにするためです。planの失敗を握りつぶすのではなく、失敗の詳細をレビュアーに届けることが目的です。
2. applyワークフロー(mainマージ後・手動実行対応)
# .github/workflows/terraform-apply.yml name: Terraform Apply on: push: branches: - main paths: - '**.tf' - '**.tfvars' # 緊急時や確認目的での手動実行トリガー workflow_dispatch: inputs: confirm: description: '本番applyを実行します。"yes"と入力して確認してください' required: true default: 'no' permissions: contents: read id-token: write jobs: apply: name: Terraform Apply runs-on: ubuntu-latest # GitHub Environmentsの「production」環境を指定 # Required reviewersを設定することで承認フローが有効になる environment: production # 手動実行時は"yes"の確認入力がなければ実行しない if: github.event_name == 'push' || github.event.inputs.confirm == 'yes' defaults: run: working-directory: ./terraform steps: - name: Checkout uses: actions/checkout@v4 - name: Configure AWS Credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/TerraformCIRole aws-region: ap-northeast-1 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.8.5 - name: Terraform Init run: terraform init - name: Terraform Apply run: terraform apply -auto-approve -input=false - name: Notify Slack on failure if: failure() uses: slackapi/slack-github-action@v1.27.0 with: payload: | { "text": ":x: Terraform Apply が失敗しました。\nリポジトリ: ${{ github.repository }}\nブランチ: ${{ github.ref_name }}\nコミット: ${{ github.sha }}\nジョブログ: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
`workflow_dispatch` トリガーを追加することで、mainブランチへのpushがなくても手動でapplyを実行できるようになります。「インフラ側で手動変更が入ってTerraformに取り込みたい」「前回のapplyがタイムアウトしてリトライしたい」といった場面で重宝します。手動実行でも `environment: production` の承認フローが適用されるため、誰でも自由に実行できるわけではありません。
末尾の `if: failure()` ステップは、apply(またはinit)が失敗したときにのみ実行されます。applyが途中でエラーになっても、インフラが中途半端な状態になっていることをチームに即時通知できます。「apply失敗に誰も気づかずインフラが壊れたまま翌朝を迎える」という状況を防ぐためのセーフティネットとして組み込んでおくことを推奨します。
3. fmt・validateをplanの前段ゲートとして設計する
planワークフローでは `terraform fmt -check` と `terraform validate` をplanより前のステップに配置しています。この順序には意図があります。・fmt -check(フォーマットチェック):コードのインデントや空行が標準フォーマットに沿っているかをチェックします。`continue-on-error: true` を設定してplanを止めないようにしつつ、結果をPRコメントに表示して可視化します。
・validate(構文検証):tfファイルの構文エラーを検出します。validateに失敗するとplanも必ず失敗するため、早期にエラーを発見できます。
・plan(実際の差分確認):AWSと通信して実際に変更される内容を確認します。前段のvalidateを通過したコードだけがplanまで到達します。
fmt-checkの `continue-on-error: true` は、フォーマット違反があってもplanを続行して差分を確認できるようにするためです。「フォーマットが崩れているが、差分の内容自体は確認したい」というケースに対応しています。
4. apply失敗時のSlack通知を組み込む
パイプラインが失敗しても誰も気づかない、という状況はCI/CD運用で最も避けたいパターンです。applyワークフローに `if: failure()` 条件のSlack通知ステップを追加することで、失敗を即時に検知できます。Slack通知で使うWebhook URLは必ずGitHub Secretsに登録します。`SLACK_WEBHOOK_URL` という名前でRepository secretsに登録するか、本番専用の通知であればEnvironment secretsに登録してください。
# ghコマンドでSlack Webhook URLをSecretsに登録する gh secret set SLACK_WEBHOOK_URL # 対話形式で値を貼り付けてEnterで確定 # 登録を確認する gh secret list # 出力例: # SLACK_WEBHOOK_URL Updated 2026-08-15
GitHub Environmentsで承認フローを設定する
1. Environmentの作成
GitHubリポジトリの「Settings」→「Environments」→「New environment」から `production` という名前のEnvironmentを作成します。作成後、「Required reviewers」に承認者を追加します。ここに登録されたユーザーまたはチームがapplyを承認するまで、ワークフローのapplyジョブは「Waiting」状態で停止します。
「Deployment protection rules」の「Prevent self-review」を有効にしておくことも推奨です。PRを作成したエンジニア本人がapplyを承認できないようにし、必ず別の人間のレビューが入る設計にできます。
Environment設定画面では「Environment secrets」も管理できます。リポジトリ全体のRepository secretsとは異なり、Environment secretsはEnvironmentが指定されたジョブ(ここでは `environment: production` のapplyジョブ)のみがアクセスできます。本番環境専用のDB接続情報・外部サービスAPIキー・Slack通知用WebhookのURLを本番applyジョブにだけ渡したい場合は、Repository secretsではなくEnvironment secretsに登録するとより安全です。
2. 承認フローの動作確認
mainブランチにマージが行われると、以下の流れで処理が進みます。・mainへのpushをトリガーにterraform-apply.ymlのワークフローが起動する
・`environment: production` の設定により、ジョブの実行前にRequired reviewersへ承認依頼の通知が送られる
・承認者がGitHubのUIで「Approve and deploy」を選択するとジョブが再開し、terraform applyが実行される
・承認者が「Reject」するとジョブはキャンセルされ、applyは実行されない
承認依頼はメール通知またはGitHubのIn-app通知で届きます。承認の有効期限(Deployment protection rules内のTimer)も設定可能で、一定時間以内に承認がなければ自動でタイムアウトさせることができます。緊急のインフラ変更でも「承認者が翌朝まで気づかなかった」という状況を防ぐには、Timerを業務時間に合わせて設定しておくのが実務上のコツです。
CI/CDパイプラインで出がちなエラーと対処
1. terraform init が失敗する
よくある原因は、backendの設定に使っているS3バケットやDynamoDBテーブルへのアクセス権限不足です。IAMロールのポリシーに以下のアクションが含まれているか確認します。# initに必要なS3・DynamoDB操作権限の確認 # s3バケット: GetObject, PutObject, DeleteObject, ListBucket # DynamoDB: GetItem, PutItem, DeleteItem(stateロック用) # IAMポリシーのシミュレーションで確認する aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::123456789012:role/TerraformCIRole \ --action-names s3:GetObject s3:PutObject dynamodb:GetItem \ --resource-arns arn:aws:s3:::your-tfstate-bucket/*
2. planで「No changes」になり差分が出ない
CI環境とローカルで異なる変数ファイル(`.tfvars`)を参照していないか確認します。GitHub Actionsのワークフローでは `-var-file` オプションを明示して、どの変数ファイルを使うかをコードで管理することを推奨します。# 変数ファイルを明示して指定する terraform plan -var-file="environments/prod.tfvars" -out=tfplan # GitHub Secretsに格納したセンシティブ変数を渡す場合 terraform plan \ -var="db_password=${{ secrets.DB_PASSWORD }}" \ -out=tfplan
3. OIDC認証エラー(Error assuming role)
`Error assuming role: AssumeRoleWithWebIdentity` が出た場合は、以下を順に確認します。・IDプロバイダーのARN:trust-policy.jsonの `Federated` フィールドに設定したARNが実際のIDプロバイダーのARNと一致しているか
・subの条件:trust-policy.jsonの `sub` 条件にリポジトリ名やブランチ名の指定ミスがないか。`repo:` の後はGitHubのオーナー名/リポジトリ名の形式です。
・AWSアカウントID:trust-policy.jsonやワークフロー内のARNに正しいAWSアカウントIDが入っているか
# 実際に受け取っているsub値を確認するには、ワークフローに以下を追加してログ確認する - name: Debug OIDC Token Sub run: | echo "ACTIONS_ID_TOKEN_REQUEST_URL: $ACTIONS_ID_TOKEN_REQUEST_URL" TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value) echo $TOKEN | cut -d '.' -f2 | base64 -d 2>/dev/null | jq .sub
4. GitHub Freeプラン(プライベートリポジトリ)でEnvironment承認が使えない
GitHub Freeプランでは、プライベートリポジトリの場合はEnvironment保護ルール(Required reviewers)を利用できません。GitHub Pro以上、またはGitHub Teamsが必要です。パブリックリポジトリであれば無料プランでも利用できます。プライベートリポジトリで承認フローを使いたい場合の代替手段として、applyワークフローのトリガーを `push` から `workflow_dispatch` のみに変更し、「実行する人間が都度判断して手動トリガーする」運用に切り替える方法があります。GitHubのプランアップグレードが難しい場合は、この方式でapplyの一元化と証跡記録だけを先に実現し、承認フローは後から追加するのが現実的です。
5. applyキャンセル後にstateロックが残る
terraform applyの実行中にGitHub Actionsのジョブを手動でキャンセルすると、DynamoDBのstateロックが解放されずに残ることがあります。次のplanやapplyを実行しようとすると、以下のようなエラーが出てすべての操作がブロックされます。# stateロックが残っている場合のエラー例 Error: Error acquiring the state lock Error message: ConditionalCheckFailedException: The conditional request failed Lock Info: ID: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 Path: your-tfstate-bucket/terraform.tfstate Operation: OperationTypeApply Who: runner@fv-az123-456 Version: 1.8.5 Created: 2026-08-06 10:30:00.000000000 +0000 UTC Info: # ロックを解除するには terraform force-unlock を使う # ロックIDはエラーメッセージのID欄からコピーする terraform force-unlock f81d4fae-7dec-11d0-a765-00a0c91e6bf6
本記事のまとめ
GitHub ActionsでのTerraform CI/CD構築における設計ポイントをまとめます。| 設計項目 | 推奨設定 |
|---|---|
| planの実行タイミング | PR作成・更新時(pull_requestトリガー) |
| planの結果共有 | actions/github-scriptでPRコメントに投稿 |
| applyの実行タイミング | mainへのpushトリガー+Environment承認後 |
| 手動apply(緊急時) | workflow_dispatchトリガー+確認入力+承認フロー |
| 本番承認フロー | GitHub Environments + Required reviewers |
| AWS認証方式 | OIDCによる一時トークン(アクセスキー不要) |
| IAMロールのsub条件 | 本番用ロールはmainブランチのみに限定 |
| 変数ファイル | -var-fileで環境ごとのtfvarsを明示指定 |
| apply失敗の通知 | if: failure()でSlack通知ステップを追加 |
| Environment secrets | 本番専用の機密情報はEnvironment secretsに格納 |
まずはplanワークフローだけを導入してPRへのコメント投稿を実現し、レビュアーが差分を確認できる環境を作るところから始めることをお勧めします。次のステップとしてapplyワークフローとEnvironment承認フローを追加すると、段階的にCI/CDを整備できます。
Terraformのstateバックエンド(S3+DynamoDB)の設定がまだの場合は、CI/CD導入前に「Terraformのtfstate管理とS3バックエンド設定」を先に確認することをお勧めします。また、providerのバージョン管理とterraform.lock.hclの扱いについては「Terraformのprovider設定とバージョン管理」を参照してください。
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます