CI/CD Entegrasyonu: Terraform ile Pipeline Kurulumu
Altyapıyı kod olarak yönetmek güzel bir fikir, ama bunu her geliştirici kendi dizüstünden terraform apply komutuyla çalıştırdığında işler hızla karmaşaya dönüşür. Kim ne zaman ne değiştirdi? Hangi state dosyası güncel? Neden prod ortamında beklenmedik bir değişiklik çıktı? İşte bu soruların cevabı CI/CD pipeline’larına Terraform entegrasyonunda yatıyor. Bu yazıda, Terraform’u otomatik bir pipeline içine nasıl gömebileceğinizi, nelere dikkat etmeniz gerektiğini ve gerçek dünya senaryolarında karşılaşılan sorunları ele alacağız.
Neden Terraform’u Pipeline’a Taşımalısınız?
Manuel terraform apply komutları birkaç kişilik küçük bir ekipte idare eder, ama ölçeklendiğinizde sorunlar da büyür. Pipeline entegrasyonu size şunları kazandırır:
- Tutarlılık: Her değişiklik aynı süreçten geçer, kimsenin local ortamındaki farklı provider versiyonu sistemi bozmaz
- Gözlemlenebilirlik: Her plan ve apply çıktısı loglanır, kim ne zaman ne yaptı tam olarak görünür
- Güvenlik: AWS/Azure credential’ları artık geliştiricilerin local makinelerinde değil, CI/CD sisteminin secret store’unda
- Peer review:
terraform plançıktısı PR üzerinde herkes tarafından incelenebilir - Otomatik doğrulama: Format kontrolü, güvenlik taraması, policy check adımları otomatik çalışır
Temel Pipeline Yapısı
Terraform pipeline’ının temel adımları şu şekilde düşünülebilir:
- Init: Provider’ları ve module’leri indir, backend’e bağlan
- Validate: Sözdizimi ve mantık kontrolü yap
- Format Check: Kod stilini doğrula
- Plan: Değişiklikleri hesapla, çıktıyı kaydet
- Apply: Plan dosyasını uygula (genellikle sadece main branch için)
Bu adımları sırasıyla inceleyelim ve gerçek pipeline örnekleriyle pekiştirelim.
GitHub Actions ile Terraform Pipeline
GitHub Actions, Terraform entegrasyonu için en yaygın kullanılan platformlardan biri. Aşağıdaki örnek, PR açıldığında plan çalıştıran, merge sonrası apply yapan tam bir workflow:
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches:
- main
paths:
- 'terraform/**'
pull_request:
branches:
- main
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'eu-west-1'
TF_WORKING_DIR: './terraform'
jobs:
terraform-check:
name: Terraform Validate & Plan
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/TerraformCIRole
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
working-directory: ${{ env.TF_WORKING_DIR }}
run: terraform init -input=false
- name: Terraform Format Check
working-directory: ${{ env.TF_WORKING_DIR }}
run: terraform fmt -check -recursive
- name: Terraform Validate
working-directory: ${{ env.TF_WORKING_DIR }}
run: terraform validate
- name: Terraform Plan
working-directory: ${{ env.TF_WORKING_DIR }}
run: |
terraform plan
-input=false
-out=tfplan
-var-file="environments/prod.tfvars"
- name: Upload Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: ${{ env.TF_WORKING_DIR }}/tfplan
retention-days: 1
terraform-apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: terraform-check
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/TerraformCIRole
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Download Plan Artifact
uses: actions/download-artifact@v4
with:
name: tfplan
path: ${{ env.TF_WORKING_DIR }}
- name: Terraform Init
working-directory: ${{ env.TF_WORKING_DIR }}
run: terraform init -input=false
- name: Terraform Apply
working-directory: ${{ env.TF_WORKING_DIR }}
run: terraform apply -input=false tfplan
Bu workflow’da dikkat edilmesi gereken nokta OIDC authentication kullanılması. Access key/secret key yerine IAM role assume ederek kimlik doğrulama yapıyoruz. Bu çok daha güvenli bir yaklaşım.
GitLab CI ile Terraform Pipeline
GitLab CI kullanan ekipler için benzer bir yapıyı .gitlab-ci.yml dosyasında şöyle kurabilirsiniz:
# .gitlab-ci.yml
image:
name: hashicorp/terraform:1.6.0
entrypoint: [""]
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/production
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- ${TF_ROOT}/.terraform/
stages:
- validate
- plan
- apply
before_script:
- cd ${TF_ROOT}
- terraform init
-backend-config="address=${TF_ADDRESS}"
-backend-config="lock_address=${TF_ADDRESS}/lock"
-backend-config="unlock_address=${TF_ADDRESS}/lock"
-backend-config="username=gitlab-ci-token"
-backend-config="password=${CI_JOB_TOKEN}"
-backend-config="lock_method=POST"
-backend-config="unlock_method=DELETE"
-backend-config="retry_wait_min=5"
validate:
stage: validate
script:
- terraform validate
- terraform fmt -check -recursive
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
plan:
stage: plan
script:
- terraform plan -out=plan.tfplan
- terraform show -json plan.tfplan > plan.json
artifacts:
name: plan
paths:
- ${TF_ROOT}/plan.tfplan
- ${TF_ROOT}/plan.json
expire_in: 1 week
reports:
terraform: ${TF_ROOT}/plan.json
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
apply:
stage: apply
script:
- terraform apply -input=false plan.tfplan
dependencies:
- plan
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
environment:
name: production
GitLab’ın güzel yanı, reports: terraform direktifi sayesinde plan çıktısını doğrudan MR üzerinde görselleştirmesi. Hangi resource’ların ekleneceği, değiştirileceği ya da silineceği MR arayüzünde görünür hale gelir.
State Dosyası Yönetimi
CI/CD ortamında en kritik konulardan biri state dosyasının güvenli ve merkezi bir yerde tutulması. Local state dosyası ile çalışmak pipeline’da kabusa dönüşür. Remote backend konfigürasyonu şöyle yapılır:
# backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/infrastructure/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
DynamoDB tablosunu oluşturmak için de yine Terraform kullanabilirsiniz (bootstrap durumu olarak ayrı bir küçük Terraform projesi):
# bootstrap/main.tf - State altyapısını kurmak için
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_dynamodb_table" "terraform_state_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Bu bootstrap’i bir kerelik olarak elle çalıştırırsınız, sonrasında her şey otomatiğe bağlanır.
Terraform Plan Çıktısını PR Yorumuna Eklemek
Ekip içi inceleme süreçleri için plan çıktısını otomatik olarak PR yorumuna eklemek büyük kolaylık sağlar. GitHub Actions’ta bunu şöyle yapabilirsiniz:
# Plan çıktısını PR yorumuna ekleyen adım
- name: Terraform Plan Output
id: plan
working-directory: ${{ env.TF_WORKING_DIR }}
run: |
terraform plan -no-color -input=false
-var-file="environments/prod.tfvars" 2>&1 | tee plan_output.txt
echo "plan_output<<EOF" >> $GITHUB_OUTPUT
cat plan_output.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Comment Plan on PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const planOutput = `${{ steps.plan.outputs.plan_output }}`;
const truncated = planOutput.length > 65000
? planOutput.substring(0, 65000) + 'nn... (Cikti cok uzun, tam log icin Actions sekmesini kontrol edin)'
: planOutput;
const comment = `## Terraform Plan Sonucu
```
${truncated}
```
*Workflow: `${{ github.workflow }}`, Commit: `${{ github.sha }}`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
Bu sayede PR’ı inceleyen herhangi biri altyapıda ne değişeceğini kod değişikliğiyle birlikte görebilir.
Güvenlik Taraması: tfsec ve Checkov
Pipeline’a güvenlik taraması eklemek artık bir lüks değil, zorunluluk. tfsec ve checkov bu iş için en popüler araçlar:
# GitHub Actions'ta tfsec entegrasyonu
- name: tfsec Security Scan
uses: aquasecurity/[email protected]
with:
working_directory: ./terraform
soft_fail: false
github_token: ${{ secrets.GITHUB_TOKEN }}
format: sarif
additional_args: >
--minimum-severity MEDIUM
--exclude-path "terraform/modules/legacy"
- name: Upload tfsec SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
Checkov’u tercih ediyorsanız:
# Checkov ile policy as code
- name: Checkov Policy Scan
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
framework: terraform
output_format: sarif
output_file_path: checkov-results.sarif
skip_check: CKV_AWS_20,CKV_AWS_28
soft_fail: false
compact: true
quiet: false
skip_check parametresini dikkatli kullanın. Bir check’i atlamak için iyi bir nedeniniz olmalı ve bu kararı yorumda ya da commit mesajında belgelemeniz gerekir.
Çoklu Ortam Yönetimi
Gerçek dünyada tek bir prod ortamı yoktur. Dev, staging, prod akışı için workspace ya da directory bazlı yapı kurabilirsiniz. Directory bazlı yaklaşım genellikle daha temiz olur:
terraform/
modules/
vpc/
eks/
rds/
environments/
dev/
main.tf
terraform.tfvars
backend.tf
staging/
main.tf
terraform.tfvars
backend.tf
prod/
main.tf
terraform.tfvars
backend.tf
Pipeline’da ortam bazlı deploy için matrix strategy kullanabilirsiniz:
# Matrix ile çoklu ortam pipeline
jobs:
terraform:
name: Terraform ${{ matrix.environment }}
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets[format('AWS_ROLE_{0}', matrix.environment)] }}
aws-region: eu-west-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.6.0'
- name: Terraform Init
working-directory: terraform/environments/${{ matrix.environment }}
run: terraform init -input=false
- name: Terraform Plan
working-directory: terraform/environments/${{ matrix.environment }}
run: |
terraform plan
-input=false
-out=tfplan
-var-file="terraform.tfvars"
- name: Terraform Apply
working-directory: terraform/environments/${{ matrix.environment }}
if: github.ref == 'refs/heads/main'
run: terraform apply -input=false tfplan
Prod ortamı için when: manual ya da GitHub’ın environment protection rules’unu kullanarak onay mekanizması ekleyin. Prod’a otomatik apply, çoğu ekip için kabul edilemez bir risk.
Sık Karşılaşılan Sorunlar ve Çözümleri
State lock sorunu: İki pipeline aynı anda çalışırsa state lock hatası alırsınız. DynamoDB tablosunu doğru kurduğunuzdan emin olun ve pipeline’da concurrent çalışmayı concurrency direktifiyle kısıtlayın:
concurrency:
group: terraform-production
cancel-in-progress: false
cancel-in-progress: false önemli. Bir apply çalışırken onu iptal etmek yarım bırakılmış altyapıya yol açar.
Provider version uyumsuzluğu: Farklı pipeline runner’larında farklı provider versiyonları indirilmesini önlemek için terraform.lock.hcl dosyasını mutlaka git’e commit edin. Bu dosyayı .gitignore‘a eklemeyin.
Uzun plan süreleri: Büyük altyapılarda plan çok zaman alabilir. -target parametresiyle sadece değişen modülleri hedefleyebilirsiniz, ama bu yöntemi dikkatli kullanın çünkü bağımlılıkları atlayabilirsiniz.
Hassas çıktılar: Terraform plan çıktısında şifreler, access key’ler gibi hassas değerler görünebilir. sensitive = true işaretlediğiniz variable’lar plan çıktısında maskelenir, ama bu her zaman yeterli olmayabilir. Log’ları kim görebiliyor, bunu düzenli kontrol edin.
OPA ile Policy as Code
Organizasyon genelinde kurallar koymak istiyorsanız Open Policy Agent (OPA) ile Conftest’i pipeline’a ekleyebilirsiniz:
# policies/terraform.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.tags.Environment
msg := sprintf("S3 bucket '%s' icin Environment tag zorunludur", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
resource.change.after.instance_type == "t2.micro"
resource.change.after.tags.Environment == "production"
msg := sprintf("Production ortaminda t2.micro kullanimi yasaktir: %s", [resource.address])
}
Bu policy’yi pipeline’da çalıştırmak için:
- name: OPA Policy Check
run: |
terraform show -json tfplan > plan.json
conftest test plan.json
--policy policies/
--all-namespaces
--output github
Bu sayede “production’da t2.micro kullanılmaz” ya da “tüm bucket’ların Environment tag’i olmalı” gibi kuralları kod olarak yönetebilirsiniz.
Atlantis: Pull Request Automation
Eğer her şeyi PR’a bağlamak istiyorsanız Atlantis’e bakmanızı öneririm. Atlantis, GitHub/GitLab webhook’larını dinler ve PR yorumlarıyla Terraform’u tetikler. atlantis plan, atlantis apply gibi yorumlar yazmak yeterli olur.
Atlantis’in pipeline yaklaşımından farkı, state lock’ı daha akıllıca yönetmesi ve PR bazlı granüler kontrol sunmasıdır. Ama kendi sunucunuzda çalışması gerektiği için ek altyapı maliyeti var.
Pipeline Performansını Artırmak
Büyük Terraform projelerinde pipeline sürelerini kısaltmak önemlidir:
- Provider binary’lerini ve
.terraformdizinini agresif şekilde cache’leyin - Module’leri cache’lemek için artifact store kullanın
- Monorepo yapısında sadece değişen dizinleri tetiklemek için
pathsfiltresini kullanın terraform planyerineterraform plan -refresh=falsekullanmak zaman kazandırır ama state’in güncel kalmasını başka mekanizmalarla sağlamanız gerekir
Sonuç
Terraform’u CI/CD pipeline’ına entegre etmek başlangıçta karmaşık görünse de doğru yapıldığında ekibin en büyük kaygılarından birini ortadan kaldırır: “Kim, ne zaman, prod’a ne attı?” sorusu artık her adımda loglanmış, onaylanmış ve tekrarlanabilir bir süreçle cevaplanır.
Öncelik sırasına koyarsak şöyle bir yol izleyin: Önce remote state’i kurun ve state lock’ı aktive edin. Sonra basit bir validate/plan workflow’u ekleyin. Ardından PR’lara plan çıktısı yorumunu entegre edin. Güvenlik taramasını ekleyin. Son olarak multi-environment yapısını oturtun. Her adımı küçük tutarak ilerleyin ve ekibin sürece alışmasını bekleyin.
Manuel terraform apply komutu çalıştıran tek bir kişi artık dar boğaz olmaktan çıkar, her geliştirici altyapı değişikliklerini güvenle önerebilir hale gelir. Bu, DevOps’un gerçek hedefidir.
