CI/CD'de Terraform: GitHub Eylemleri, Atlantis ve Çekme İsteği İş Akışı
Otomatik tüy bırakma ve doğrulamadan profesyonel bir Terraform işlem hattı oluşturun Planlama/uygulama için GitHub Eylemleri ile Atlantis ile Çekme İsteği iş akışı planlanan sürüklenme tespitine kadar işbirlikçi.
Terraform Neden Özel Bir Boru Hattı Gerektirir?
Koşmak terraform apply bir geliştiricinin dizüstü bilgisayarından manuel olarak
Altyapıdaki en tehlikeli model. Otomatik bir iş akışı olmadan sorunlar
Tipik durumlar şunlardır: durum dosyası güncellenmedi, yerel kimlik bilgileri CI ortamından farklı,
başvuru öncesinde planın incelenmesi yok, kimin neyi, ne zaman uyguladığına dair bir iz yok.
Profesyonel bir Terraform boru hattı tüm bu sorunları çözer: plan gelir Çekme İsteğinde otomatik olarak oluşturulur, incelenir ve başvuru ancak daha sonra gerçekleşir Versiyonlama sistemindeki kalıcı günlüklerle onay. Bu makalede Bu boru hattını iki yaklaşımla oluşturuyoruz: GitHub Eylemleri takımlar için küçük ve Atlantis orta ve büyük takımlar için.
Ne Öğreneceksiniz
- GitHub Eylemleri ardışık düzeni: fmt, validate, tflint, PR planı yapma ve birleştirmede uygulama
- OIDC ile güvenli bulut kimlik bilgisi yönetimi (uzun ömürlü sırlar yok)
- Atlantis: kurulum, atlantis.yaml ve işbirliğine dayalı PR iş akışı
- Yönetilen bir alternatif olarak Terraform Cloud / HCP Terraform
- Sürüklenme tespiti: durum ve gerçek altyapı arasındaki tutarsızlıkları tespit etmek için cron işi
- Çoklu ortam ardışık düzenleri için matris stratejisi (geliştirme/aştırma/üretim)
- Planlara ve sürüklenme uyarılarına ilişkin Slack/Teams bildirimleri
Terraform Deposunun Yapısı
İşlem hattını oluşturmadan önce net bir depo yapısına ihtiyacınız var. Desen en yaygın olanı çevreye göre farklı dizinlere ayrılmasıdır, ayrı bir dizinde paylaşılan modüllerle.
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 Eylemleri: İşlem Hattını Tamamlayın
Oluşturduğumuz GitHub Actions işlem hattının iki iş akışı vardır: biri Çekme İstekleri (lint + plan) tarafından tetiklenir ve diğeri ana öğedeki birleştirme (uygulama) tarafından tetiklenir.
# .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: Uzun Ömürlü, Sırsız Kimlik Bilgileri
GitHub Eylemlerinin kimliğini AWS'de doğrulamak için modern en iyi uygulama şudur:
OIDC (OpenID Bağlantısı): GitHub, AWS'nin imzalayacağı imzalı bir JWT belirteci oluşturur
ezberlemene gerek kalmadan kabul et AWS_ACCESS_KEY_ID sırlar içinde.
# 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: Halkla İlişkiler Yoluyla İşbirliği Planlayın/Uygulayın
Atlantis GitHub/GitLab/Bitbucket web kancalarını dinleyen bir sunucudur
ve PR'lerdeki yorumlara yanıt verir. Yazdığında atlantis plan bir yorumda,
Atlantis infaz ediyor terraform plan ve çıktıyla yanıt verir. Bu bir yaratır
iş akışı tamamen izlenir ve PR'de incelenebilir.
# 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:
Sürüklenme Algılama: Tutarsızlıkları Algılamak için Cron Job
Il sürüklenme gerçek altyapının farklı olduğu zamandır Terraform durum dosyasında açıklananlardan. Sürüklenme olur Birisi kaynakları AWS konsolundan manuel olarak düzenlediğinde, bir kaynak harici bir işlem tarafından veya bir bulut API'sinin davranışı değiştiğinde değiştirilir.
# .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 için Gelişmiş Linting
# .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 Bulutu: Yönetilen Alternatif
Atlantis'i kendi başınıza yönetmek istemiyorsanız, HCP Dünya Biçimi (eski adıyla Terraform Cloud) uzaktan planlama/uygulama, durum yönetimi, SSO ve politikayı (Sentinel) hizmet olarak sunar.
# 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
Üretimde Terraform Boru Hatları için En İyi Uygulamalar
Terraform Boru Hattı Kontrol Listesi
- Amerika OIDC CI sırlarında uzun ömürlü AWS kimlik bilgileri yerine
- Koşmak
terraform fmt -checkbiçimlendirilmemiş kodla PR'yi engellemek için - Amerika
terraform validatesözdizimi hatalarını yakalama planından önce - İkili planı kaydedin (
-out=tfplan) ve bunu uygulamak için kullanın, yenilenmez - Uygulamayı paralel olarak değil sıralı sırayla (geliştirme → aşamalandırma → üretim) çalıştırın
- GitHub Ortam koruma kurallarıyla üretim için manuel onay isteyin
- Manuel değişiklikleri tespit etmek için planlı sapma tespitini uygulayın
- Eklemek
prevent_destroy = truekritik durum bilgisi olan kaynaklar hakkında - Tüm planları günlüğe kaydedin ve zaman damgasıyla uygulayın ve CI günlüğünde yazar
Sonuçlar ve Sonraki Adımlar
İyi tasarlanmış bir Terraform boru hattı bir altyapının temelidir güvenli ve izlenebilir. PR'deki Planla-İncele-Uygula iş akışı, hiçbirinin Altyapı değişikliği gözden geçirilmeden gerçekleşirken sürüklenme tespiti Planlanmamış değişiklikleri sorun haline gelmeden yakalayın.
Serideki Sonraki Yazılar
- Madde 5: IaC Testi — Terratest, Terraform Testi Yerel e Sözleşme Testi: Terraform modülleriniz için otomatik testler nasıl yazılır?
- Madde 6: IaC Güvenliği — Checkov, Trivy ve OPA Kod Olarak Politika: güvenlik taramasını satış hattının ön başvuru kapısına entegre eder.







