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.
serialparametresiyle 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.
