CI/CD での Terraform: GitHub アクション、Atlantis、プル リクエストのワークフロー
プロフェッショナルな Terraform パイプラインを構築する: 自動 lint と検証から GitHub アクションを使用したプル リクエストのワークフローと Atlantis による計画/適用 計画的なドリフト検出まで協力して行います。
Terraform に専用のパイプラインが必要な理由
走る terraform apply 開発者のラップトップから手動で
インフラストラクチャにおける最も危険なパターン。自動化されたワークフローがないと問題が発生する
典型的なものは次のとおりです: 状態ファイルが更新されていない、ローカル認証情報が CI 環境と異なる、
申請前に計画を検討することはなく、誰がいつ何を申請したのかの痕跡もありません。
プロフェッショナルな Terraform パイプラインがこれらすべての問題を解決します: 計画が実現します 自動的に生成され、プル リクエストでレビューされ、適用はその後にのみ行われます 承認、バージョン管理システムの永続的なログ。この記事では このパイプラインは次の 2 つのアプローチで構築します。 GitHub アクション チーム用 小さくて アトランティス 中規模および大規模なチーム向け。
何を学ぶか
- GitHub Actions パイプライン: fmt、validate、tflint、PR の計画、およびマージの適用
- OIDC を使用した安全なクラウド資格情報管理 (長期間有効なシークレットなし)
- Atlantis: インストール、atlantis.yaml、および共同 PR ワークフロー
- 管理された代替手段としての Terraform Cloud / HCP Terraform
- ドリフト検出: 状態と実際のインフラストラクチャ間の不一致を検出する cron ジョブ
- マルチ環境パイプラインのマトリックス戦略 (開発/ステージング/本番)
- 計画に関する Slack/Teams の通知とドリフト アラート
Terraform リポジトリの構造
パイプラインを構築する前に、明確なリポジトリ構造が必要です。パターン 最も一般的なのは、環境ごとに個別のディレクトリに分割することです。 モジュールは別のディレクトリで共有されます。
terraform-infra/
modules/
vpc/
main.tf
variables.tf
outputs.tf
eks-cluster/
rds-postgres/
environments/
dev/
main.tf # Usa i moduli con variabili dev
variables.tf
terraform.tfvars
backend.tf # Backend S3 per dev
staging/
main.tf
terraform.tfvars
backend.tf
production/
main.tf
terraform.tfvars
backend.tf
.terraform-version # tfenv: specifica la versione di Terraform
.tflint.hcl # Configurazione tflint
.github/
workflows/
terraform.yml
# .terraform-version
# Usato da tfenv per installare la versione corretta
1.7.5
GitHub アクション: 完全なパイプライン
私たちが構築する GitHub Actions パイプラインには 2 つのワークフローがあります。 1 つはプル リクエスト (lint + plan) によってトリガーされ、もう 1 つはメインでのマージ (適用) によってトリガーされます。
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
pull_request:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
permissions:
contents: read
pull-requests: write # Per commentare il plan sul PR
id-token: write # Per OIDC authentication con AWS
env:
TF_VERSION: '1.7.5'
TFLINT_VERSION: 'v0.50.0'
jobs:
# Job 1: Lint e validazione statica (tutti gli ambienti in parallelo)
lint-validate:
name: Lint & Validate (${{ matrix.environment }})
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, production]
fail-fast: false
defaults:
run:
working-directory: environments/${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Setup TFLint
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: ${{ env.TFLINT_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
working-directory: .
- name: Terraform Init (solo backend locale per lint)
run: terraform init -backend=false
- name: Terraform Validate
run: terraform validate
- name: TFLint
run: |
tflint --init
tflint --format compact
# Job 2: Plan su Pull Request
plan:
name: Plan (${{ matrix.environment }})
runs-on: ubuntu-latest
needs: lint-validate
if: github.event_name == 'pull_request'
strategy:
matrix:
environment: [dev, staging] # Non pianifichiamo prod su ogni PR
fail-fast: false
defaults:
run:
working-directory: environments/${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
# OIDC: nessun secret long-lived necessario
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsterraform-${{ matrix.environment }}
aws-region: eu-west-1
- name: Terraform Init
run: terraform init
- name: Terraform Plan
id: plan
run: |
terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt
echo "PLAN_EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_ENV"
# Commenta il plan sul Pull Request
- name: Comment Plan on PR
uses: actions/github-script@v7
if: always()
with:
script: |
const fs = require('fs');
const planOutput = fs.readFileSync('environments/${{ matrix.environment }}/plan_output.txt', 'utf8');
const truncated = planOutput.length > 65000
? planOutput.substring(0, 65000) + '\n... (truncated)'
: planOutput;
const body = `## Terraform Plan - ${{ matrix.environment }}
\`\`\`
${truncated}
\`\`\`
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
// Cerca commenti precedenti dello stesso workflow e aggiorna
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = comments.data.find(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes('Terraform Plan - ${{ matrix.environment }}')
);
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
# Job 3: Apply su merge in main
apply:
name: Apply (${{ matrix.environment }})
runs-on: ubuntu-latest
needs: lint-validate
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
strategy:
# Apply sequenziale: prima dev, poi staging, poi production
max-parallel: 1
matrix:
environment: [dev, staging, production]
environment:
name: ${{ matrix.environment }} # Richiede approvazione manuale per production
defaults:
run:
working-directory: environments/${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsterraform-${{ matrix.environment }}
aws-region: eu-west-1
- name: Terraform Init
run: terraform init
- name: Terraform Apply
run: terraform apply -auto-approve -no-color
OIDC: シークレットのない認証情報は長期間存続します
GitHub Actions を AWS に認証するための最新のベストプラクティスは次のとおりです。
OIDC (OpenID コネクト): GitHub は、AWS が署名した JWT トークンを生成します。
暗記する必要なく受け入れる AWS_ACCESS_KEY_ID 秘密で。
# Configurazione IAM Role per GitHub Actions OIDC
# Crea questo con Terraform stesso (bootstrap)
# iam.tf
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
}
resource "aws_iam_role" "github_actions_terraform" {
for_each = toset(["dev", "staging", "production"])
name = "GitHubActionsterraform-${each.key}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Permetti solo da questo repo specifico
"token.actions.githubusercontent.com:sub" = "repo:myorg/terraform-infra:*"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "terraform_permissions" {
for_each = aws_iam_role.github_actions_terraform
role = each.value.name
policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
# In produzione usa policy più ristrette con least-privilege
}
アトランティス: PR によるコラボレーションの計画/申請
アトランティス GitHub/GitLab/Bitbucket Webhook をリッスンするサーバーです
PR のコメントに応答します。書くとき atlantis plan コメントで、
アトランティスが処刑される terraform plan そして出力で応答します。これにより、
ワークフローは完全に追跡され、PR でレビュー可能です。
# atlantis.yaml - Configurazione nella root del repository
version: 3
# Abilita auto-planning: piano automatico quando il PR tocca file .tf
automerge: false
parallel_plan: true
parallel_apply: false
projects:
- name: dev
dir: environments/dev
workspace: default
autoplan:
when_modified: ["*.tf", "*.tfvars", "../../modules/**/*.tf"]
enabled: true
apply_requirements:
- approved # Richiede almeno 1 approvazione
- mergeable # Il PR deve essere mergeable
- name: staging
dir: environments/staging
workspace: default
autoplan:
when_modified: ["*.tf", "*.tfvars", "../../modules/**/*.tf"]
enabled: true
apply_requirements:
- approved
- mergeable
- name: production
dir: environments/production
workspace: default
autoplan:
enabled: false # Non pianifica automaticamente in prod
apply_requirements:
- approved # Richiede almeno 2 approvazioni
- mergeable
allowed_override_dentists: # Solo questi utenti possono fare apply in prod
- alice
- bob
# Comandi Atlantis nel Pull Request (come commento)
# Esegui plan per il progetto 'dev'
atlantis plan -p dev
# Esegui plan per tutti i progetti modificati
atlantis plan
# Applica dopo approvazione del PR
atlantis apply -p dev
# Annulla un plan in corso
atlantis unlock
# Mostra lo stato attuale
atlantis status
# docker-compose.yml per Atlantis self-hosted
version: '3.8'
services:
atlantis:
image: ghcr.io/runatlantis/atlantis:v0.27.0
ports:
- "4141:4141"
environment:
ATLANTIS_GH_USER: atlantis-bot # GitHub user del bot
ATLANTIS_GH_TOKEN: ${GH_TOKEN} # Personal Access Token del bot
ATLANTIS_GH_WEBHOOK_SECRET: ${WEBHOOK_SECRET}
ATLANTIS_REPO_ALLOWLIST: "github.com/myorg/terraform-infra"
ATLANTIS_AUTOMERGE: "false"
ATLANTIS_WRITE_GIT_CREDS: "true"
# Credenziali AWS (oppure usa IAM Instance Profile)
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION: eu-west-1
volumes:
- atlantis-data:/home/atlantis
- ./atlantis.yaml:/home/atlantis/atlantis.yaml:ro
volumes:
atlantis-data:
ドリフト検出: 不一致を検出するための Cron ジョブ
Il ドリフト それは実際のインフラストラクチャが異なる場合です Terraform 状態ファイルに記述されている内容から。ドリフトが起こる 誰かが AWS コンソールからリソースを手動で編集したとき、リソースが 外部プロセスによって、またはクラウド API の動作が変更されたときに変更されます。
# .github/workflows/drift-detection.yml
name: Terraform Drift Detection
on:
schedule:
- cron: '0 8 * * 1-5' # Ogni giorno lavorativo alle 8:00 UTC
workflow_dispatch: # Esecuzione manuale
permissions:
contents: read
issues: write # Per aprire issue se drift rilevato
id-token: write
jobs:
detect-drift:
name: Detect Drift (${{ matrix.environment }})
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, production]
fail-fast: false
defaults:
run:
working-directory: environments/${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.7.5'
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsReadOnly-${{ matrix.environment }}
aws-region: eu-west-1
- name: Terraform Init
run: terraform init -no-color
- name: Terraform Plan (Drift Detection)
id: drift
run: |
# Esci con codice 0 se no changes, 1 se error, 2 se changes (drift)
terraform plan -detailed-exitcode -no-color 2>&1 | tee drift_output.txt
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_ENV"
- name: Alert on Drift
if: env.EXIT_CODE == '2'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('environments/${{ matrix.environment }}/drift_output.txt', 'utf8');
// Crea un issue GitHub per il drift rilevato
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '[DRIFT] Infrastructure drift detected in ${{ matrix.environment }}',
body: `## Drift rilevato in ambiente: ${{ matrix.environment }}
Data: ${new Date().toISOString()}
\`\`\`
${output.substring(0, 60000)}
\`\`\`
**Azione richiesta:** Analizza le modifiche e riconcilia lo state.
- Se la modifica è intenzionale: aggiorna il codice Terraform e fai un PR
- Se è una modifica non autorizzata: ripristina con \`terraform apply\``,
labels: ['infrastructure', 'drift', '${{ matrix.environment }}'],
});
- name: Notify Slack on Drift
if: env.EXIT_CODE == '2'
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": ":warning: Terraform drift rilevato in *${{ matrix.environment }}*!\nControlla il <${{ github.server_url }}/${{ github.repository }}/issues|GitHub Issue> creato.",
"channel": "#infra-alerts"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
TFLint: HCL の高度なリンティング
# .tflint.hcl - Configurazione TFLint
plugin "aws" {
enabled = true
version = "0.29.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
plugin "azurerm" {
enabled = true
version = "0.26.0"
source = "github.com/terraform-linters/tflint-ruleset-azurerm"
}
# Regole generali
rule "terraform_deprecated_interpolation" {
enabled = true
}
rule "terraform_documented_outputs" {
enabled = true
}
rule "terraform_documented_variables" {
enabled = true
}
rule "terraform_naming_convention" {
enabled = true
format = "snake_case"
}
rule "terraform_required_providers" {
enabled = true
}
rule "terraform_required_version" {
enabled = true
}
# Regole AWS specifiche
rule "aws_instance_invalid_type" {
enabled = true
}
rule "aws_instance_previous_type" {
enabled = true
}
Terraform Cloud: マネージド型の代替手段
アトランティスを自分で管理したくない場合は、 HCP テラフォーム (以前の Terraform クラウド) リモート プラン/適用、状態管理、SSO、およびポリシー (Sentinel) をサービスとして提供します。
# terraform.tf - Configurazione HCP Terraform come backend
terraform {
required_version = "~> 1.7"
# HCP Terraform (gestisce state, plan e apply in cloud)
cloud {
organization = "my-organization"
workspaces {
# Workspace per environment con tagging
tags = ["terraform-infra", "aws"]
}
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Con HCP Terraform:
# - Nessun backend S3 da gestire
# - Plan/Apply avvengono nei server HCP (non in CI)
# - Variabili e secrets gestiti nel workspace
# - VCS integration: trigger automatico da PR
# - Team-based access control
# - Run history permanente
本番環境における Terraform パイプラインのベスト プラクティス
Terraform パイプラインのチェックリスト
- アメリカ合衆国 OIDC CI シークレット内の長期間有効な AWS 認証情報の代わりに
- 走る
terraform fmt -checkフォーマットされていないコードを含む PR をブロックするには - アメリカ合衆国
terraform validate構文エラーを捕捉する計画の前に - バイナリ プランを保存します (
-out=tfplan)それを適用に使用すると、再生成されません - 適用は並列ではなく、順番に (開発→ステージング→本番) 実行します。
- GitHub 環境保護ルールを使用して本番環境の手動承認をリクエストする
- 手動による変更を検出するためにスケジュールされたドリフト検出を実装する
- 追加
prevent_destroy = true重要なステートフル リソースについて - すべての計画を記録し、CI ログにタイムスタンプと作成者を付けて適用します。
結論と次のステップ
適切に設計された Terraform パイプラインはインフラストラクチャの基盤です 安全で追跡可能。 PR の計画、確認、適用のワークフローにより、 インフラストラクチャの変更はレビューなしで行われますが、ドリフトの検出は行われません。 計画外の変更が問題になる前にキャッチします。
シリーズの次の記事
- 第5条: IaC テスト — Terratest、Terraform Test Native 契約テスト: Terraform モジュールの自動テストを作成する方法。
- 第6条: IaC セキュリティ — Checkov、Trivy、OPA コードとしてのポリシー: セキュリティ スキャンをパイプラインの事前適用ゲートに統合します。







