はじめに
第16回では、Terraformのコードをモジュール化し、ディレクトリ分離によって環境を適切に管理する方法を学びました。また、第17回では状態ファイルをS3に保存するリモートバックエンドを設定し、チーム全体で状態を共有して同時実行を防ぐロック機構を実現しました。
これらの仕組みにより、Terraformをチーム開発で使用するための基盤は整いましたが、terraformコマンドの操作は依然として手動操作に依存しています。
例えば、プルリクエストで変更内容をレビューする際、コードを見るのは当然ですが、コードと実環境の差分を確認するのも忘れてはなりません。しかし、この差分を確認するにはterraform planを実行する必要があり、この操作を各自が手元で行うのは非効率でしょう。また、mainブランチへのマージ後、誰かが手動でterraform applyを実行する必要があります。本番環境であれば代表者がタイミングを見計らってapplyするのも悪くないですが、開発環境やステージング環境ではマージと共に自動でapplyを実行できた方がDevOps的です。
コードを書いたらセキュリティスキャンも必要です。セキュリティチェックのような定型的なコマンドを実行する処理、特に時間のかかる処理は、各自が手元で実行するよりもCIで自動実行する方が効率的でしょう。
本記事では、これらの課題を解決するために、GitHub ActionsでのCI/CD統合について解説します。プルリクエスト時にterraform planを自動実行してレビューし、mainブランチへのマージをトリガーにterraform applyを自動実行することで、開発環境への変更を即時反映できます。これはCI/CDを使用する大きなメリットです。
一方で、本番環境へのapplyは上長の承認が必要だったり、デプロイのタイミングをコントロールしたい場合もあります。そこで、開発環境は自動デプロイ、本番環境は承認フロー付きでデプロイする構成を紹介します。より保守的な運用(applyは常に手動実行)を希望する場合は、ワークフローからterraform-applyジョブを削除し、必要に応じてローカルまたはGitHub ActionsのWorkflow Dispatch機能を使って手動実行してください。
ディレクトリ分離の構成では、変更されたファイルから対象環境を検出し、その環境のみにTerraformを実行する必要があります。
GitHub ActionsでCI/CD統合
Terraformをチーム開発で使用する際、CI/CDパイプラインとの統合は不可欠です。以下のようなワークフローを実装します。
ワークフローの設計
-
プルリクエスト時
- 変更されたファイルから対象環境を検出
- 対象環境に対して
terraform fmtでコードフォーマットをチェック -
terraform validateで構文をチェック -
terraform planを実行し、変更内容をコメントに投稿
-
mainブランチへのマージ時
- 変更されたファイルから対象環境を検出
- 対象環境に対して
terraform applyを実行(本番環境は承認後に実行)
AWS認証情報の設定
GitHub ActionsからAWSにアクセスするため、OIDCを使った認証を設定します。これは、長期的なアクセスキーを使用するよりも安全です。
ステップ1: IAMロールの作成
AWSマネジメントコンソールまたはAWS CLIで、GitHub Actions用のIAMロールを作成します。詳細はAWS公式ガイドを参照してください。
{
"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:*"
}
}
}
]
}
123456789012はAWSアカウントID、your-orgはGitHub組織名またはユーザー名、your-repoはリポジトリ名に置き換えてください。
IAMロールを作成します。
$ aws iam create-role \
--role-name GitHubActionsRole \
--assume-role-policy-document file://trust-policy.json
必要な権限をアタッチします。
$ aws iam attach-role-policy \
--role-name GitHubActionsRole \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
本番環境では最小権限の原則に従って、必要な権限のみを付与するカスタムポリシーを作成することを推奨します。本記事では、ポリシー設定の複雑さを避けるためAdministratorAccessをアタッチしていますが、実運用では適切に権限を絞り込んでください。
ステップ2: GitHub Actionsワークフローの作成
リポジトリに.github/workflows/terraform.ymlを作成します。このワークフローは、変更されたファイルから対象環境を検出し、その環境に対してのみTerraformを実行します。
name: Terraform CI/CD
on:
pull_request:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
push:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
permissions:
id-token: write # OIDC認証のためのトークン発行に必要
contents: read # リポジトリのコードを読み取るために必要
pull-requests: write # PRにコメントを投稿するために必要
jobs:
detect-changes:
name: Detect Changed Environments
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.detect.outputs.environments }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Detect changed environments
id: detect
run: |
# PRの場合とpushの場合で比較対象のコミットを切り替え
if [ "${{ github.event_name }}" == "pull_request" ]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
else
BASE_SHA="${{ github.event.before }}"
HEAD_SHA="${{ github.sha }}"
fi
# 変更されたファイルから環境名を抽出し、JSON配列に変換
CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)
ENVIRONMENTS=$(echo "$CHANGED_FILES" | grep -E '^environments/[^/]+' | cut -d'/' -f2 | sort -u | jq -R -s -c 'split("\n")[:-1]')
# モジュールが変更された場合は全環境を対象にする
# 共通モジュールの変更はすべての環境に影響するため
if echo "$CHANGED_FILES" | grep -q '^modules/'; then
ENVIRONMENTS='["dev","prod"]'
fi
echo "environments=$ENVIRONMENTS" >> $GITHUB_OUTPUT
echo "Detected environments: $ENVIRONMENTS"
terraform-plan:
name: Terraform Plan
needs: detect-changes
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.environments != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
environment: ${{ fromJson(needs.detect-changes.outputs.environments) }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
# 環境ごとに異なるAWSアカウントを使用する場合の例
# prod環境: 111111111111(本番環境のAWSアカウントID)
# dev環境: 222222222222(開発環境のAWSアカウントID)
# 同じAWSアカウントを使用する場合は、三項演算子を使わず直接ARNを指定してください
# 例: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
role-to-assume: ${{ matrix.environment == 'prod' && 'arn:aws:iam::111111111111:role/GitHubActionsRole' || 'arn:aws:iam::222222222222:role/GitHubActionsRole' }}
aws-region: ap-northeast-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.13.5
- name: Terraform Format Check
id: fmt
run: terraform fmt -check -recursive
working-directory: .
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
working-directory: ./environments/${{ matrix.environment }}
- name: Terraform Validate
id: validate
run: terraform validate
working-directory: ./environments/${{ matrix.environment }}
# Trivyセキュリティスキャン
- name: Run Trivy security scanner
uses: aquasecurity/trivy-action@0.30.0
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '0' # セキュリティ問題が見つかってもワークフローを継続(警告として扱う)
# 本番運用でCRITICAL/HIGHを検出時にPRをブロックしたい場合は '1' に設定
severity: 'CRITICAL,HIGH,MEDIUM'
scanners: 'misconfig,secret'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: ./environments/${{ matrix.environment }}
continue-on-error: true
- name: Comment PR
uses: actions/github-script@v8
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `### Terraform Plan - \`${{ matrix.environment }}\` Environment
#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
<details><summary>Validation Output</summary>
\`\`\`
${{ steps.validate.outputs.stdout }}
\`\`\`
</details>
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
terraform-apply:
name: Terraform Apply
needs: detect-changes
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.detect-changes.outputs.environments != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
environment: ${{ fromJson(needs.detect-changes.outputs.environments) }}
# この行により、GitHub Environmentsで設定した承認フローが適用されます
# 本番環境(prod)の場合は承認者の承認が必要になります
environment: ${{ matrix.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
# 環境ごとに異なるAWSアカウントを使用する場合の例
# prod環境: 111111111111(本番環境のAWSアカウントID)
# dev環境: 222222222222(開発環境のAWSアカウントID)
# 同じAWSアカウントを使用する場合は、三項演算子を使わず直接ARNを指定してください
# 例: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
role-to-assume: ${{ matrix.environment == 'prod' && 'arn:aws:iam::111111111111:role/GitHubActionsRole' || 'arn:aws:iam::222222222222:role/GitHubActionsRole' }}
aws-region: ap-northeast-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.13.5
- name: Terraform Init
run: terraform init
working-directory: ./environments/${{ matrix.environment }}
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: ./environments/${{ matrix.environment }}
このワークフローは、以下のように動作します。
まず、変更検出ジョブ(detect-changes)で変更されたファイルから対象環境を検出します。environments/dev/配下のファイルが変更されていればdevを、modules/配下が変更されていれば、すべての環境を出力します。
次に、プルリクエスト時のterraform-planジョブでは、検出された環境ごとに並列実行(matrix strategy)します。環境ごとに適切なAWS認証情報を使用し、terraform planの結果を環境ごとにPRにコメントします。
最後に、mainブランチへのマージ時のterraform-applyジョブでは、検出された環境ごとにterraform applyを実行します。本番環境(prod)は承認が必要です(environment: ${{ matrix.environment }})。
承認フローの追加
本番環境への変更は慎重に行う必要があります。GitHub Actionsのenvironment機能を使って、環境ごとに承認フローを設定できます。
ステップ1: Environment設定
GitHubリポジトリの「Settings」→「Environments」から、環境ごとにEnvironmentを作成します。GitHub Environmentsの詳細は公式ドキュメントを参照してください。
-
dev: 承認不要(自動デプロイ) -
prod: 2名以上の承認が必要
devは承認不要なため、特別な設定は不要です。下図の通り、デフォルト設定のまま進めていきます。
prodは2名以上の承認が必要なため、ステップ2以降の手順を実行し承認を有効化していきましょう。
ステップ2: Required reviewersの設定
本番環境(prod)には「Required reviewers」を有効にし、シニアエンジニアやインフラチームのメンバーを承認者として指定します。これにより、terraform applyの実行前に指定したメンバーの承認が必要になります。
ステップ3: Environment protection rulesの設定
さらに、以下の保護ルールを追加できます。
- Wait timer: デプロイ前に一定時間待機させる
- Deployment branches: 特定のブランチからのみデプロイを許可
・ワークフローでの動作
上記のワークフローではenvironment: ${{ matrix.environment }}を指定しているため、環境ごとに適切な承認フローが適用されます。
- 開発環境への変更: mainへのマージ後、即座に
terraform applyが実行 - 本番環境への変更: mainへのマージ後、承認者の承認を待ってから
terraform applyが実行
PRをマージするとEnvironmentsの設定でレビュアーに追加した人にレビュー依頼のメールが送信されます。そこに記載のリンクから「Approve and deploy」をクリックすることでデプロイが動き始めます。
これにより、開発環境では迅速なイテレーションを維持しつつ、本番環境では慎重なデプロイを実現できます。
セキュリティスキャンの詳細
ワークフローに含まれている「Trivy」はAqua Securityが提供する包括的なセキュリティスキャナーで、tfsecの後継として推奨されています。IaCの設定ミス、脆弱性、シークレットの検出が可能です。
Trivyステップの設定を詳しく見てみましょう。
- name: Run Trivy security scanner
uses: aquasecurity/trivy-action@0.30.0
with:
scan-type: 'config' # IaCスキャンを実行
scan-ref: '.' # カレントディレクトリをスキャン
format: 'sarif' # SARIF形式で出力
output: 'trivy-results.sarif'
exit-code: '0' # エラーでもワークフローを継続
severity: 'CRITICAL,HIGH,MEDIUM' # 検出する深刻度
scanners: 'misconfig,secret' # 設定ミスとシークレットを検出
Trivyは、AWSリソースのセキュリティベストプラクティスに違反していないかをチェックします。例えば、以下のような問題を検出します。
- S3バケットが公開設定になっていないか
- セキュリティグループが0.0.0.0/0からのアクセスを許可していないか
- シークレットがハードコードされていないか
- EKSクラスターのログが有効化されているか
- IAMロールが過度な権限を持っていないか
結果はSARIF形式で出力され、GitHubのSecurityタブ > Code scanningでPR番号を指定すると確認できます。または、PR画面のChecksタブ > Code scanning results > Trivy > View all branch alertsでも確認できます。
検出された問題は優先度(CRITICAL、HIGH、MEDIUM、LOW)に応じて対処します。CRITICALやHIGHの問題はマージ前に必ず修正することを推奨します。
ワークフローの実行例
実際のワークフローの動作を確認してみましょう。
プルリクエスト時の動作
-
プルリクエストを作成
開発環境のEKSクラスターのノード数を1から2に増やすPRを作成します。# environments/dev/main.tf module "eks" { source = "../../modules/eks" # ... node_desired_size = 2 # 1から2に変更 # ... } -
自動的にワークフローが実行される
GitHub Actionsが自動的に起動し、以下のステップを実行します。- 変更検出:
dev環境が変更されたことを検出 - Terraform Format Check: コードフォーマットをチェック
- Terraform Init: 初期化
- Terraform Validate: 構文をチェック
- Trivy: セキュリティスキャン
- Terraform Plan: 変更内容を確認
- 変更検出:
-
PRにコメントが投稿される
terraform planの結果がPRにコメントされ、レビュアーが変更内容を確認できます。
mainブランチへのマージ時の動作
-
PRをマージ
レビューが完了し、PRをmainブランチにマージします。 -
開発環境への自動デプロイ
mainブランチへのマージをトリガーに、GitHub Actionsが自動的にterraform applyを実行します。開発環境(dev)は承認不要なため、即座にデプロイされます。 -
本番環境への承認待ち
本番環境(prod)のファイルも変更されている場合、承認者に通知が送信されます。承認者が「Approve and deploy」をクリックすることで本番環境へのデプロイが開始されます。
おわりに
本記事では、GitHub ActionsでのTerraform CI/CD統合について解説しました。
プルリクエスト時にterraform planを自動実行してレビューし、mainブランチへのマージ後にterraform applyを自動実行することで開発環境への変更を即時反映できるようになりました。また、本番環境への変更には承認フローを設け、安全なデプロイを実現しました。Trivyによるセキュリティスキャンにより、設定ミスやシークレットの漏洩を事前に防ぐことができます。
第16回から第18回までの3回シリーズを通じて、Terraformをチーム開発で活用するためのベストプラクティスを学びました。
- 第16回: モジュール化とディレクトリ分離による環境管理
- 第17回: リモートバックエンドによる状態管理とロック機構
- 第18回: CI/CDパイプラインとの統合と承認フロー
これらの仕組みを導入することで、以下のメリットが得られます。
- 協調作業の実現: チーム全員が同じ状態を共有し、安全に作業できる
- 変更の可視化: プルリクエストで変更内容をレビューできる
- 環境の分離: 開発・本番環境を適切に分離し、誤操作を防げる
- 安全なデプロイ: 承認フローにより、本番環境への変更を慎重に行える
- 自動化: 手動操作を減らし、人的ミスを防げる
- 監査証跡: GitとCIログにより、すべての変更を追跡できる
特に重要なのは、Terraformワークスペースを環境分離に使わないという点です。公式ドキュメントでも明記されている通り、ワークスペースは同じバックエンドと認証情報を共有するため、セキュリティの隔離メカニズムとしては不十分です。環境分離にはディレクトリ分離とモジュール化の組み合わせを使用しましょう。
最初は設定が複雑に感じるかもしれませんが、一度構築してしまえば、その後の運用は大幅に効率化されます。小さなプロジェクトから始めて、徐々に適用範囲を広げていくことをお勧めします。
次回は、Terraformで構築したインフラの運用監視とコスト管理について解説します。「AWS CloudWatch」や「Cost Explorer」と連携して、インフラの健全性とコストを継続的に監視する方法を学びます。
- この記事のキーワード