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 .terraform dizinini 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 paths filtresini kullanın
  • terraform plan yerine terraform plan -refresh=false kullanmak 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir