CI/CD Pipeline ile Ansible Entegrasyonu: Otomatik Dağıtım Rehberi

Modern yazılım geliştirme dünyasında “bende çalışıyor” problemi artık kabul edilebilir bir mazeret değil. Geliştirici makinesinden production ortamına kadar her adımın otomatik, tekrarlanabilir ve güvenilir olması gerekiyor. İşte tam bu noktada CI/CD pipeline’ları ile Ansible’ın birlikteliği devreye giriyor. Bu yazıda, gerçek dünya senaryoları üzerinden nasıl sağlam bir entegrasyon kurulacağını adım adım ele alacağız.

Neden Ansible ve CI/CD Birlikte Kullanılmalı?

Ansible’ı tek başına kullanmak zaten büyük bir kazanım. Playbook’larını çalıştırıyorsun, sunucular istediğin duruma geliyor. Ama burada şu soru çıkıyor ortaya: Bu playbook’ları kim, ne zaman, hangi sırayla çalıştıracak?

Manuel çalıştırma yaklaşımı birkaç ciddi sorun barındırıyor. Bir ekip üyesi eski versiyonu çalıştırabilir, test edilmemiş değişiklikler production’a gidebilir ya da hangi playbook’un ne zaman çalıştırıldığının kaydı tutulmayabilir. CI/CD entegrasyonu bu sorunların tamamını çözüyor:

  • Tekrarlanabilirlik: Her deployment aynı adımlardan geçiyor
  • Audit trail: Kim, ne zaman, hangi değişikliği deploy etti
  • Otomatik test: Playbook’lar production’a gitmeden önce syntax ve lint kontrolünden geçiyor
  • Rollback kolaylığı: Pipeline geçmişinden istediğin versiyona dönebiliyorsun
  • Ekip uyumu: Herkes aynı süreci kullanıyor, “bende farklı çalışıyor” problemi ortadan kalkıyor

Mimari Genel Bakış

Tipik bir entegrasyonda üç katman var. Kaynak kod deposu (GitLab, GitHub, Bitbucket), CI/CD motoru (Jenkins, GitLab CI, GitHub Actions) ve hedef altyapı. Ansible bu üç katman arasında köprü görevi görüyor.

Şu akışı düşün: Bir developer feature branch’ına Ansible playbook değişikliği pushlıyor. CI sistemi tetikleniyor, syntax kontrolü yapılıyor, staging ortamında test çalıştırılıyor, onay mekanizması devreye giriyor ve production’a deploy ediliyor. Tüm bu süreç otomatik ve izlenebilir.

Proje Yapısını Kurmak

Önce mantıklı bir dizin yapısı oluşturalım. Ansible projelerinde düzen çok kritik, özellikle birden fazla kişi çalışıyorsa.

ansible-project/
├── inventories/
│   ├── staging/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   └── production/
│       ├── hosts.yml
│       └── group_vars/
├── roles/
│   ├── webserver/
│   ├── database/
│   └── monitoring/
├── playbooks/
│   ├── deploy-app.yml
│   ├── setup-infra.yml
│   └── rollback.yml
├── .gitlab-ci.yml          # veya Jenkinsfile
├── ansible.cfg
└── requirements.yml

ansible.cfg dosyası pipeline boyunca tutarlı davranış için kritik:

# ansible.cfg
[defaults]
inventory = inventories/staging/hosts.yml
remote_user = deploy
private_key_file = ~/.ssh/deploy_key
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callback_whitelist = timer, profile_tasks

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

pipelining = True ayarı SSH bağlantı sayısını azaltarak pipeline sürelerini ciddi ölçüde kısaltıyor. Büyük altyapılarda bu farkı çok net hissediyorsun.

GitLab CI ile Entegrasyon

GitLab CI en yaygın kullanılan sistemlerden biri, o yüzden buradan başlayalım. Önce temel bir .gitlab-ci.yml yapısı:

# .gitlab-ci.yml
image: python:3.11-slim

variables:
  ANSIBLE_HOST_KEY_CHECKING: "False"
  ANSIBLE_FORCE_COLOR: "True"
  PY_COLORS: "1"

stages:
  - validate
  - test
  - deploy-staging
  - approve
  - deploy-production

before_script:
  - pip install ansible ansible-lint
  - mkdir -p ~/.ssh
  - echo "$DEPLOY_SSH_KEY" | tr -d 'r' > ~/.ssh/deploy_key
  - chmod 600 ~/.ssh/deploy_key
  - eval $(ssh-agent -s)
  - ssh-add ~/.ssh/deploy_key

syntax-check:
  stage: validate
  script:
    - ansible-playbook --syntax-check playbooks/deploy-app.yml
    - ansible-playbook --syntax-check playbooks/setup-infra.yml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

lint-check:
  stage: validate
  script:
    - ansible-lint playbooks/
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-staging:
  stage: deploy-staging
  script:
    - ansible-playbook -i inventories/staging/hosts.yml
        playbooks/deploy-app.yml
        --extra-vars "app_version=$CI_COMMIT_SHORT_SHA"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

deploy-production:
  stage: deploy-production
  script:
    - ansible-playbook -i inventories/production/hosts.yml
        playbooks/deploy-app.yml
        --extra-vars "app_version=$CI_COMMIT_SHORT_SHA"
  environment:
    name: production
    url: https://example.com
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Burada dikkat edilmesi gereken birkaç nokta var. SSH key’i CI/CD değişkeni olarak saklıyorsun ve before_script içinde geçici olarak ekliyorsun. when: manual ile production deployment’ı insan onayına bağlıyorsun. Bu kritik bir güvenlik katmanı.

Jenkins ile Entegrasyon

Jenkins kullananlar için Declarative Pipeline yaklaşımı daha okunaklı ve yönetilebilir:

// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'python:3.11-slim'
            args '-v /tmp:/tmp'
        }
    }

    environment {
        ANSIBLE_HOST_KEY_CHECKING = 'False'
        DEPLOY_ENV = "${env.BRANCH_NAME == 'main' ? 'production' : 'staging'}"
    }

    stages {
        stage('Setup') {
            steps {
                sh 'pip install ansible ansible-lint --quiet'
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'deploy-ssh-key',
                    keyFileVariable: 'SSH_KEY'
                )]) {
                    sh '''
                        mkdir -p ~/.ssh
                        cp $SSH_KEY ~/.ssh/deploy_key
                        chmod 600 ~/.ssh/deploy_key
                    '''
                }
            }
        }

        stage('Validate') {
            parallel {
                stage('Syntax Check') {
                    steps {
                        sh 'ansible-playbook --syntax-check playbooks/deploy-app.yml'
                    }
                }
                stage('Lint') {
                    steps {
                        sh 'ansible-lint playbooks/'
                    }
                }
            }
        }

        stage('Deploy Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh """
                    ansible-playbook -i inventories/staging/hosts.yml 
                        playbooks/deploy-app.yml 
                        --extra-vars "app_version=${env.GIT_COMMIT[0..7]}"
                """
            }
        }

        stage('Smoke Test') {
            when {
                branch 'develop'
            }
            steps {
                sh 'ansible-playbook playbooks/smoke-test.yml -i inventories/staging/hosts.yml'
            }
        }

        stage('Deploy Production') {
            when {
                branch 'main'
            }
            steps {
                input message: 'Production deploy onaylıyor musunuz?',
                      submitter: 'devops-lead,sre-team'
                sh """
                    ansible-playbook -i inventories/production/hosts.yml 
                        playbooks/deploy-app.yml 
                        --extra-vars "app_version=${env.GIT_COMMIT[0..7]}"
                """
            }
        }
    }

    post {
        failure {
            sh 'ansible-playbook playbooks/rollback.yml -i inventories/${DEPLOY_ENV}/hosts.yml'
            slackSend channel: '#deployments',
                      message: "FAILED: ${env.JOB_NAME} - ${env.BUILD_URL}"
        }
        success {
            slackSend channel: '#deployments',
                      message: "SUCCESS: ${env.JOB_NAME} deploy tamamlandı"
        }
    }
}

Jenkins’in post bloğu özellikle değerli. Deployment başarısız olduğunda otomatik rollback ve Slack bildirimi gönderiyor. Gece 3’te production alarm vermeden önce bu mekanizma seni kurtarabilir.

Ansible Vault ile Secret Yönetimi

CI/CD pipeline’larında en sık yapılan hata: secret’ları düz metin olarak environment variable veya playbook içinde tutmak. Ansible Vault bunu çözüyor.

# Vault şifresi oluştur ve CI/CD sistemine ekle
ansible-vault create inventories/production/group_vars/all/vault.yml

# Vault dosyası içeriği (şifreli tutulacak)
vault_db_password: "super_secret_password"
vault_api_key: "production_api_key_here"
vault_ssl_cert: |
  -----BEGIN CERTIFICATE-----
  ...
  -----END CERTIFICATE-----

Pipeline içinde vault şifresini güvenli kullanmak için:

# GitLab CI örneği - vault entegrasyonu
deploy-production:
  stage: deploy-production
  script:
    - echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass
    - chmod 600 /tmp/vault_pass
    - ansible-playbook -i inventories/production/hosts.yml
        playbooks/deploy-app.yml
        --vault-password-file /tmp/vault_pass
        --extra-vars "app_version=$CI_COMMIT_SHORT_SHA"
    - rm -f /tmp/vault_pass
  after_script:
    - rm -f /tmp/vault_pass

after_script kullanımı önemli. Ana script başarısız olsa bile vault şifre dosyası temizleniyor. Güvenlik açısından bu detay çok kritik.

Dinamik Inventory ile Bulut Entegrasyonu

Statik inventory dosyaları küçük altyapılar için yeterli, ama AWS, Azure veya GCP kullanıyorsan dinamik inventory kaçınılmaz. Pipeline’ı buna göre ayarlamak gerekiyor:

# requirements.yml - gerekli koleksiyonlar
---
collections:
  - name: amazon.aws
    version: ">=6.0.0"
  - name: community.general
    version: ">=7.0.0"
  - name: ansible.posix
    version: ">=1.5.0"
# inventories/aws/aws_ec2.yml - dinamik AWS inventory
plugin: amazon.aws.aws_ec2
regions:
  - eu-west-1
  - eu-central-1
filters:
  instance-state-name: running
  "tag:Environment": "{{ lookup('env', 'DEPLOY_ENV') }}"
keyed_groups:
  - key: tags.Role
    prefix: role
  - key: tags.Environment
    prefix: env
hostnames:
  - private-ip-address
compose:
  ansible_host: private_ip_address

Pipeline’da bu inventory’yi kullanmak için:

# CI script içinde
export AWS_ACCESS_KEY_ID=$CI_AWS_ACCESS_KEY
export AWS_SECRET_ACCESS_KEY=$CI_AWS_SECRET_KEY
export DEPLOY_ENV="production"

# Koleksiyonları yükle
ansible-galaxy collection install -r requirements.yml

# Dinamik inventory ile çalıştır
ansible-playbook -i inventories/aws/aws_ec2.yml 
  playbooks/deploy-app.yml 
  --limit "role_webserver:&env_production"

Gerçek Dünya Senaryosu: Blue-Green Deployment

Basit bir deploy playbook’u yerine gerçekten kullanışlı bir şeye bakalım. Blue-green deployment hem zero-downtime hem de hızlı rollback sağlıyor:

# playbooks/blue-green-deploy.yml
---
- name: Blue-Green Deployment
  hosts: loadbalancer
  gather_facts: no
  vars:
    app_version: "{{ lookup('env', 'APP_VERSION') }}"

  tasks:
    - name: Aktif ortamı tespit et
      shell: cat /etc/nginx/active-env
      register: active_env
      changed_when: false

    - name: Hedef ortamı belirle
      set_fact:
        target_env: "{{ 'blue' if active_env.stdout == 'green' else 'green' }}"

    - name: Yeni versiyonu hedef gruba deploy et
      include_role:
        name: webserver
      vars:
        deployment_env: "{{ target_env }}"
        version: "{{ app_version }}"
      delegate_to: "{{ item }}"
      loop: "{{ groups['webservers_' + target_env] }}"

    - name: Health check - yeni deployment
      uri:
        url: "http://{{ item }}:8080/health"
        method: GET
        status_code: 200
        timeout: 30
      loop: "{{ groups['webservers_' + target_env] }}"
      retries: 5
      delay: 10

    - name: Load balancer'ı yeni ortama yönlendir
      template:
        src: nginx-lb.conf.j2
        dest: /etc/nginx/conf.d/app.conf
      vars:
        active_servers: "{{ groups['webservers_' + target_env] }}"

    - name: Nginx'i yeniden yükle
      service:
        name: nginx
        state: reloaded

    - name: Aktif ortam kaydını güncelle
      copy:
        content: "{{ target_env }}"
        dest: /etc/nginx/active-env

Bu playbook’u pipeline’da kullanırken APP_VERSION değişkenini Git commit hash’i olarak geçiyorsun. Bir şeyler ters giderse önceki ortam hala çalışır durumda, sadece load balancer’ı geri çevirmen yeterli.

Pipeline Performansını Optimize Etmek

Büyük altyapılarda Ansible pipeline’larının yavaş olduğundan şikayet çok. Birkaç pratik optimizasyon:

# ansible.cfg - performans ayarları
[defaults]
forks = 20
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
strategy = free

[ssh_connection]
pipelining = True
control_path_dir = /tmp/ansible-cp
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s

forks = 20: Paralel olarak kaç sunucuya bağlanılacağını belirliyor. Default 5, bu çok düşük.

fact_caching: Fact toplama işlemi tekrarlanan pipeline çalıştırmalarında zaman kaybettiriyor. Cache ile bu sorun ortadan kalkıyor.

strategy = free: Bir görevi tüm sunucularda tamamlamayı beklemeden sonraki göreve geçiyor.

# Sadece değişen playbook'ları çalıştırmak için tags kullanımı
# GitLab CI örneği
deploy-webserver-only:
  stage: deploy-staging
  script:
    - ansible-playbook -i inventories/staging/hosts.yml
        playbooks/deploy-app.yml
        --tags "webserver,nginx"
  rules:
    - changes:
        - roles/webserver/**/*
        - roles/nginx/**/*

GitLab CI’ın changes kuralı çok güçlü. Sadece ilgili dosyalar değiştiğinde ilgili playbook çalışıyor. Hem pipeline süresi kısalıyor hem de gereksiz değişiklik riski azalıyor.

Hata Yönetimi ve Monitoring

Pipeline’larda hata yönetimi en çok atlanan konulardan biri. Şu yaklaşım çok işe yarıyor:

# playbooks/deploy-with-rollback.yml
---
- name: Deployment with automatic rollback
  hosts: webservers
  serial: "30%"

  pre_tasks:
    - name: Mevcut versiyonu kaydet
      shell: cat /opt/app/current_version
      register: previous_version
      changed_when: false
      ignore_errors: yes

  tasks:
    - name: Yeni versiyonu deploy et
      block:
        - include_tasks: tasks/deploy.yml
        - include_tasks: tasks/health-check.yml

      rescue:
        - name: Deployment başarısız - rollback başlıyor
          debug:
            msg: "Deployment başarısız, {{ previous_version.stdout }} versiyonuna rollback yapılıyor"

        - include_tasks: tasks/rollback.yml
          vars:
            rollback_version: "{{ previous_version.stdout }}"

        - name: Pipeline'ı başarısız olarak işaretle
          fail:
            msg: "Deployment başarısız ve rollback tamamlandı. Manuel inceleme gerekiyor."

      always:
        - name: Deployment sonucunu logla
          local_action:
            module: uri
            url: "{{ monitoring_webhook_url }}"
            method: POST
            body_format: json
            body:
              status: "{{ 'success' if ansible_failed_task is not defined else 'failed' }}"
              version: "{{ app_version }}"
              host: "{{ inventory_hostname }}"
              timestamp: "{{ ansible_date_time.iso8601 }}"

block/rescue/always yapısı Python’daki try/except/finally mantığıyla aynı. Her koşulda monitoring webhook’una bildirim gidiyor, başarısız olunca otomatik rollback tetikleniyor.

GitHub Actions ile Entegrasyon

GitHub kullananlar için Actions çok pratik bir seçenek:

# .github/workflows/ansible-deploy.yml
name: Ansible Deploy

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python kurulumu
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Ansible ve araçları yükle
        run: pip install ansible ansible-lint

      - name: Syntax kontrolü
        run: ansible-playbook --syntax-check playbooks/deploy-app.yml

      - name: Lint kontrolü
        run: ansible-lint playbooks/

  deploy-staging:
    needs: validate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging

    steps:
      - uses: actions/checkout@v4

      - name: SSH key kurulumu
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H staging.example.com >> ~/.ssh/known_hosts

      - name: Ansible kurulumu
        run: pip install ansible

      - name: Staging deployment
        run: |
          ansible-playbook -i inventories/staging/hosts.yml 
            playbooks/deploy-app.yml 
            --extra-vars "app_version=${{ github.sha }}"
        env:
          ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://example.com

    steps:
      - uses: actions/checkout@v4

      - name: SSH key kurulumu
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.PROD_DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key

      - name: Ansible kurulumu
        run: pip install ansible

      - name: Production deployment
        run: |
          ansible-playbook -i inventories/production/hosts.yml 
            playbooks/deploy-app.yml 
            --extra-vars "app_version=${{ github.sha }}"
        env:
          ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}

GitHub Actions’ta environment özelliği production için otomatik onay mekanizması sunuyor. Repository ayarlarından environment protection rules tanımlayarak belirli kişilerin onayını zorunlu kılabiliyorsun.

Yaygın Hatalar ve Çözümleri

Birkaç yıllık deneyimden derlenen “ah bu yine oldu” listesi:

SSH agent forwarding sorunları: CI container’ı içinde SSH agent düzgün başlamayabiliyor. eval $(ssh-agent -s) && ssh-add kombinasyonu çoğu durumu çözüyor ama container’ın bash yerine sh çalıştırmasına dikkat et.

Fact toplama timeout’ları: Büyük sunucu havuzlarında gather_facts: no ile başlayıp sadece ihtiyaç duyulan yerlerde setup modülü çağırmak ciddi zaman kazandırıyor.

Idempotency sorunları: shell ve command modülleri her çalıştırmada “changed” döndürüyor. Bu pipeline’ı anlamsız hale getirebilir. creates, removes parametrelerini ya da changed_when: false kullanmayı alışkanlık haline getir.

Inventory lock sorunları: Aynı anda birden fazla pipeline çalışırsa çakışma olabilir. GitLab’da resource_group, Jenkins’te lock step’i bu sorunu çözüyor.

Sonuç

Ansible ile CI/CD entegrasyonu başta karmaşık görünse de temel yapıyı bir kez oturtunca değişiklik yönetimi inanılmaz kolaylaşıyor. Önemli noktalara bir daha değinelim:

  • Secret yönetimini asla hafife alma. Ansible Vault artı CI/CD sistem secret store kombinasyonu şart.
  • Her deployment öncesi syntax ve lint kontrolünü zorunlu kıl. Pipeline birkaç dakika uzasa da production’da geçireceğin saatten çok daha ucuz.
  • Rollback mekanizmasını baştan tasarla. “Gerekirse rollback yaparız” yaklaşımı, gece yarısı production’da panik yaşatır.
  • serial parametresiyle rolling deployment kullan. Tüm sunuculara aynı anda gitmek yerine aşamalı geçiş hataları erken tespit ettiriyor.
  • Pipeline sürelerini takip et. 30 dakikayı geçen pipeline’lar ekibin CI/CD’yi bypass etmesine neden olur.

Başlangıç için tüm bu yapıyı bir anda kurmana gerek yok. Önce syntax check’i ekle, sonra staging otomasyonunu, sonra production onay mekanizmasını. Her adım bir öncekinden değer üretiyor. Asıl önemli olan başlamak.

Bir yanıt yazın

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