Terraform w CI/CD: GitHub Actions, Atlantis i przepływ pracy w trybie pull request
Zbuduj profesjonalny potok Terraform: od automatycznego lintowania i sprawdzania poprawności z GitHub Actions, aby pobrać przepływ pracy z Atlantis w celu zaplanowania/aplikacji współpracę, aż do planowanego wykrycia dryfu.
Dlaczego Terraform wymaga dedykowanego rurociągu
Uruchomić terraform apply ręcznie z laptopa programisty
najniebezpieczniejszy wzorzec w infrastrukturze. Bez zautomatyzowanego przepływu pracy, problemy
typowe to: plik stanu nie został zaktualizowany, lokalne poświadczenia różnią się od środowiska CI,
żadnego przeglądu planu przed złożeniem wniosku, żadnego śladu, kto, co i kiedy zastosował.
Profesjonalny rurociąg Terraform rozwiązuje wszystkie te problemy: pojawia się plan generowane automatycznie, sprawdzane w żądaniu ściągnięcia, a zastosowanie następuje dopiero później zatwierdzenia, ze stałymi logami w systemie wersjonowania. W tym artykule Budujemy ten rurociąg, stosując dwa podejścia: Akcje GitHuba dla drużyn mały i Atlantyda dla średnich i dużych zespołów.
Czego się nauczysz
- Potok akcji GitHub: fmt, sprawdzanie poprawności, tflint, planowanie PR i stosowanie przy łączeniu
- Bezpieczne zarządzanie danymi uwierzytelniającymi w chmurze za pomocą OIDC (bez długotrwałych tajemnic)
- Atlantis: instalacja, atlantis.yaml i wspólny przepływ pracy PR
- Terraform Cloud / HCP Terraform jako zarządzana alternatywa
- Wykrywanie dryfu: zadanie cron wykrywające rozbieżności pomiędzy stanem a rzeczywistą infrastrukturą
- Strategia macierzowa dla potoków wielośrodowiskowych (dev/staging/prod)
- Powiadomienia Slack/Teams dotyczące planów i alerty driftu
Struktura Repozytorium Terraform
Przed zbudowaniem potoku potrzebna jest przejrzysta struktura repozytorium. Wzór najczęstszym jest podział ze względu na środowisko na odrębne katalogi, z modułami udostępnionymi w oddzielnym katalogu.
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
Akcje GitHub: ukończ potok
Tworzony przez nas potok GitHub Actions składa się z dwóch przepływów pracy: jeden wywołany przez żądania ściągnięcia (lint + plan) i jeden wywołany przez połączenie na stronie głównej (zastosuj).
# .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: Długowieczne poświadczenia bez tajemnic
Nowoczesną najlepszą praktyką uwierzytelniania akcji GitHub w AWS jest
OIDC (połączenie OpenID): GitHub generuje podpisany token JWT, który AWS
zaakceptować bez konieczności zapamiętywania AWS_ACCESS_KEY_ID w tajemnicach.
# 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
}
Atlantis: Planuj/stosuj wspólnie poprzez PR
Atlantyda to serwer, który nasłuchuje webhooków GitHub/GitLab/Bitbucket
i odpowiada na komentarze w PR. Kiedy piszesz atlantis plan w komentarzu,
Atlantyda wykonuje terraform plan i odpowiada danymi wyjściowymi. To tworzy
przepływ pracy w pełni śledzony i możliwy do przeglądu w 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:
Wykrywanie dryfu: Zadanie Cron do wykrywania rozbieżności
Il dryf ma to miejsce wtedy, gdy rzeczywista infrastruktura jest inna z tego, co opisano w pliku stanu Terraform. Dryf się zdarza gdy ktoś ręcznie edytuje zasoby z konsoli AWS, gdy plik Resource zostanie zmieniony przez proces zewnętrzny lub gdy interfejs API chmury zmieni zachowanie.
# .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: Zaawansowane linting dla 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: zarządzana alternatywa
Jeśli nie chcesz sam zarządzać Atlantydą, Terraforma HCP (dawniej Terraform Cloud) oferuje zdalne planowanie/aplikowanie, zarządzanie stanem, logowanie jednokrotne i politykę (Sentinel) jako usługę.
# 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
Najlepsze praktyki dotyczące rurociągów terraformowych w produkcji
Lista kontrolna rurociągu Terraform
- USA OIDC zamiast długotrwałych poświadczeń AWS w tajemnicach CI
- Uruchomić
terraform fmt -checkaby zablokować PR za pomocą niesformatowanego kodu - USA
terraform validateprzed planem wyłapania błędów składniowych - Zapisz plan binarny (
-out=tfplan) i użyj go do zastosowania, nie regeneruje się - Uruchamiaj Apply w kolejności sekwencyjnej (dev → staging → prod), a nie równolegle
- Poproś o ręczne zatwierdzenie produkcji za pomocą zasad ochrony środowiska GitHub
- Zaimplementuj zaplanowane wykrywanie dryftu, aby wykryć ręczne zmiany
- Dodać
prevent_destroy = truena krytycznych zasobach stanowych - Rejestruj wszystkie plany i aplikuj, podając znacznik czasu i autora w dzienniku CI
Wnioski i dalsze kroki
Dobrze zaprojektowany rurociąg Terraform jest podstawą infrastruktury bezpieczne i identyfikowalne. Przepływ pracy Plan-Przejrzyj-Zastosuj w żądaniu ściągnięcia gwarantuje, że żaden modyfikacja infrastruktury następuje bez przeglądu, przy jednoczesnym wykrywaniu dryftu Wychwytuj niezaplanowane zmiany, zanim staną się problemami.
Następne artykuły z serii
- Artykuł 5: Testowanie IaC — Terratest, Terraform native test e Testowanie kontraktowe: Jak napisać automatyczne testy dla modułów Terraform.
- Artykuł 6: Bezpieczeństwo IaC — Checkov, Trivy i OPA Policy-as-Code: integruje skanowanie bezpieczeństwa z bramą przedaplikacyjną potoku.







