Ekibinizde birisi her deployment öncesinde “ben manuel yapayım, daha hızlı olur” diyorsa, bu yazıyı o kişiye gönderin. CI/CD pipeline ile Ansible entegrasyonu, bu tür tartışmaları tarihe gömen ve deployment süreçlerini tekrarlanabilir, denetlenebilir hale getiren en güçlü yaklaşımlardan biri.
Bu yazıda gerçek dünya senaryoları üzerinden, Jenkins ve GitLab CI kullanarak Ansible playbook’larını otomatik tetiklemeyi, inventory yönetimini, secret handling’i ve pipeline tasarım pratiklerini ele alacağız.
Neden CI/CD ve Ansible Birlikte?
Ansible tek başına harika bir araç. Playbook yazıyorsunuz, ansible-playbook komutu çalıştırıyorsunuz, sunucularınız istediğiniz state’e geliyor. Ama şu soruyu sorun kendinize: bu playbook’u kim çalıştırıyor, ne zaman, hangi versiyonuyla ve sonuç ne oldu?
Manuel çalıştırmalarda şu problemler kaçınılmaz oluyor:
- Kimin çalıştırdığı belli değil: “Ben çalıştırdım sanıyordum” klasiği
- Hangi branch üzerinden: Local’de değişiklik var ama push edilmemiş
- Audit trail yok: Hangi değişiklik ne zaman uygulandı?
- Test edilmemiş playbook: Syntax hatası canlıya mı çıktı?
CI/CD entegrasyonu bu sorunların hepsini çözüyor. Her değişiklik versiyon kontrolünde, her deployment loglanmış, her playbook çalışmadan önce test edilmiş durumda.
Mimari Genel Bakış
Tipik bir CI/CD + Ansible mimarisinde şu bileşenler var:
- Git Repository: Playbook’lar, roller ve inventory burada
- CI/CD Server: Jenkins, GitLab CI, GitHub Actions vb.
- Ansible Control Node: Pipeline agent’ı bu rolü üstleniyor veya ayrı bir sunucu
- Target Hosts: Yönetilecek sunucular
- Vault/Secret Manager: Ansible Vault veya HashiCorp Vault
Pipeline akışı genellikle şöyle: kod push -> syntax check -> lint -> test environment’ta dry-run -> onay (staging için) -> deployment -> notification.
Proje Yapısını Düzgün Kurmak
İyi bir entegrasyon için önce repository yapısının düzgün olması lazım. Ben şu yapıyı kullanıyorum:
ansible-repo/
├── inventories/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ └── staging/
│ ├── hosts.yml
│ └── group_vars/
│ ├── all.yml
│ └── webservers.yml
├── roles/
│ ├── nginx/
│ ├── postgresql/
│ └── app_deploy/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
├── .gitlab-ci.yml (veya Jenkinsfile)
├── ansible.cfg
└── requirements.yml
Bu yapı, farklı environment’lar için ayrı inventory tutmanızı ve pipeline’ın hangi environment’a deploy edeceğini parametrik olarak belirlemenizi sağlıyor.
ansible.cfg dosyasını da pipeline-friendly hale getirin:
# ansible.cfg
[defaults]
inventory = inventories/staging/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callbacks_enabled = timer, profile_tasks
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r
retry_files_enabled = False özellikle önemli, CI ortamında retry dosyaları hem gereksiz hem de artifact’ları kirletiyor.
GitLab CI Entegrasyonu
GitLab kullanıyorsanız .gitlab-ci.yml ile başlayalım. Gerçek bir web uygulaması deployment senaryosu:
# .gitlab-ci.yml
image: python:3.11-slim
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_FORCE_COLOR: "true"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
cache:
paths:
- .pip-cache/
stages:
- validate
- test
- deploy-staging
- deploy-production
before_script:
- pip install ansible ansible-lint
- ansible --version
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d 'r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval $(ssh-agent -s)
- ssh-add ~/.ssh/id_rsa
syntax-check:
stage: validate
script:
- ansible-playbook --syntax-check -i inventories/staging/hosts.yml playbooks/site.yml
- ansible-playbook --syntax-check -i inventories/production/hosts.yml playbooks/site.yml
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH == "develop"'
ansible-lint:
stage: validate
script:
- ansible-lint playbooks/site.yml --profile=production
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
dry-run-staging:
stage: test
script:
- echo "$VAULT_PASSWORD" > .vault_pass
- ansible-playbook -i inventories/staging/hosts.yml
--vault-password-file .vault_pass
--check --diff
playbooks/site.yml
after_script:
- rm -f .vault_pass
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
deploy-to-staging:
stage: deploy-staging
script:
- echo "$VAULT_PASSWORD" > .vault_pass
- ansible-playbook -i inventories/staging/hosts.yml
--vault-password-file .vault_pass
playbooks/site.yml
-e "app_version=$CI_COMMIT_SHORT_SHA"
after_script:
- rm -f .vault_pass
environment:
name: staging
url: https://staging.sirketiniz.com
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
deploy-to-production:
stage: deploy-production
script:
- echo "$VAULT_PASSWORD" > .vault_pass
- ansible-playbook -i inventories/production/hosts.yml
--vault-password-file .vault_pass
playbooks/site.yml
-e "app_version=$CI_COMMIT_SHORT_SHA"
after_script:
- rm -f .vault_pass
environment:
name: production
url: https://sirketiniz.com
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Burada dikkat edilecek birkaç nokta var. when: manual ile production deployment’ı otomatik değil, biri GitLab UI’dan onay verince çalışıyor. app_version değişkeni olarak commit SHA geçiriyoruz, böylece hangi kod versiyonunun deploy edildiğini tam olarak biliyoruz.
Jenkins ile Entegrasyon
Jenkins kullanıyorsanız Declarative Pipeline ile şöyle bir Jenkinsfile oluşturun:
// Jenkinsfile
pipeline {
agent {
docker {
image 'python:3.11-slim'
args '-u root'
}
}
environment {
ANSIBLE_HOST_KEY_CHECKING = 'False'
ANSIBLE_FORCE_COLOR = 'true'
VAULT_PASSWORD = credentials('ansible-vault-password')
SSH_PRIVATE_KEY = credentials('ansible-ssh-key')
}
stages {
stage('Setup') {
steps {
sh '''
pip install -q ansible ansible-lint
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ansible --version
'''
}
}
stage('Syntax Check') {
steps {
sh '''
ansible-playbook --syntax-check
-i inventories/staging/hosts.yml
playbooks/site.yml
'''
}
}
stage('Ansible Lint') {
steps {
sh 'ansible-lint playbooks/site.yml'
}
}
stage('Dry Run - Staging') {
steps {
sh '''
echo "$VAULT_PASSWORD" > .vault_pass
ansible-playbook -i inventories/staging/hosts.yml
--vault-password-file .vault_pass
--check --diff
playbooks/site.yml
rm -f .vault_pass
'''
}
}
stage('Deploy Staging') {
when {
branch 'develop'
}
steps {
sh '''
echo "$VAULT_PASSWORD" > .vault_pass
ansible-playbook -i inventories/staging/hosts.yml
--vault-password-file .vault_pass
playbooks/site.yml
-e "app_version=${GIT_COMMIT:0:8}"
rm -f .vault_pass
'''
}
}
stage('Deploy Production') {
when {
branch 'main'
}
input {
message "Production'a deploy edilsin mi?"
ok "Evet, deploy et"
submitter "admin,lead-devops"
}
steps {
sh '''
echo "$VAULT_PASSWORD" > .vault_pass
ansible-playbook -i inventories/production/hosts.yml
--vault-password-file .vault_pass
playbooks/site.yml
-e "app_version=${GIT_COMMIT:0:8}"
rm -f .vault_pass
'''
}
}
}
post {
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "Deployment başarılı: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "Deployment başarısız: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
always {
cleanWs()
}
}
}
Jenkins’te input direktifi harika bir özellik, production deployment öncesinde belirli kullanıcıların onayını zorunlu kılıyor.
Secret Yönetimi
Pipeline’da secret yönetimi en kritik konulardan biri. Birkaç yaklaşım var:
Ansible Vault + CI/CD Secret Store kombinasyonu en yaygın ve güvenli yaklaşım. Vault password’ünü CI/CD sisteminin secret store’unda tutuyorsunuz, runtime’da dosyaya yazıp Ansible’a geçiriyorsunuz, iş bitince siliyorsunuz.
# Vault şifrelenmiş değişken dosyası oluşturma
ansible-vault create inventories/production/group_vars/all/vault.yml
# Mevcut dosyayı şifreleme
ansible-vault encrypt inventories/production/group_vars/all/secrets.yml
# Pipeline'da kullanım
echo "$VAULT_PASSWORD" > /tmp/.vault_pass
ansible-playbook -i inventories/production/hosts.yml
--vault-password-file /tmp/.vault_pass
playbooks/site.yml
rm -f /tmp/.vault_pass
Vault dosyası örneği:
# inventories/production/group_vars/all/vault.yml (şifrelenmiş)
vault_db_password: "super_secret_password_123"
vault_api_key: "sk-prod-xxxxxxxxxxxx"
vault_ssl_cert: |
-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----
Bu değişkenleri normal group_vars dosyasında vault_ prefix’i olmadan referans edebilirsiniz:
# inventories/production/group_vars/all/main.yml
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
Dinamik Inventory Pipeline Entegrasyonu
Statik inventory dosyaları küçük ortamlar için işe yarıyor ama büyük ve dinamik altyapılarda AWS, GCP veya Azure’dan dinamik inventory çekmek gerekiyor.
# requirements.yml - gerekli koleksiyonları tanımlayın
---
collections:
- name: amazon.aws
version: ">=6.0.0"
- name: community.general
version: ">=7.0.0"
- name: ansible.posix
version: ">=1.5.0"
Pipeline başında koleksiyonları kurun:
# CI script içinde
pip install ansible boto3 botocore
ansible-galaxy collection install -r requirements.yml
# AWS dynamic inventory ile çalıştırma
ansible-playbook -i inventories/aws_ec2.yml
--vault-password-file .vault_pass
playbooks/webservers.yml
--limit "tag_Environment_production"
AWS EC2 dynamic inventory dosyası:
# inventories/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- eu-central-1
- eu-west-1
filters:
instance-state-name: running
"tag:ManagedBy": ansible
keyed_groups:
- prefix: env
key: tags.Environment
- prefix: role
key: tags.Role
hostnames:
- private-ip-address
compose:
ansible_host: private_ip_address
Pipeline’da Test ve Validasyon Stratejisi
Sadece syntax check yetmez, playbook’larınızı gerçekten test etmeniz lazım. Molecule ile role testing’i pipeline’a entegre edin:
# .gitlab-ci.yml'a eklenecek molecule test stage'i
molecule-test:
stage: test
image: python:3.11-slim
before_script:
- pip install ansible molecule molecule-docker docker
script:
- cd roles/nginx
- molecule test
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- roles/nginx/**/*
Role içinde molecule/default/molecule.yml:
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-test
image: "geerlingguy/docker-ubuntu2204-ansible:latest"
pre_build_image: true
- name: centos-test
image: "geerlingguy/docker-rockylinux9-ansible:latest"
pre_build_image: true
provisioner:
name: ansible
config_options:
defaults:
callbacks_enabled: "profile_tasks"
verifier:
name: ansible
Rollback Mekanizması
Deployment başarısız olursa ne yapacaksınız? Pipeline’a rollback capability ekleyin:
# GitLab CI rollback job'u
rollback-production:
stage: deploy-production
script:
- echo "$VAULT_PASSWORD" > .vault_pass
- ansible-playbook -i inventories/production/hosts.yml
--vault-password-file .vault_pass
playbooks/rollback.yml
-e "rollback_version=$ROLLBACK_VERSION"
- rm -f .vault_pass
environment:
name: production
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Rollback playbook’u basit ama etkili:
# playbooks/rollback.yml
---
- name: Application Rollback
hosts: webservers
become: yes
vars:
rollback_version: "{{ rollback_version | mandatory }}"
tasks:
- name: Mevcut versiyonu kaydet
command: readlink /var/www/app/current
register: current_version
changed_when: false
- name: Rollback versiyonuna geç
file:
src: "/var/www/app/releases/{{ rollback_version }}"
dest: "/var/www/app/current"
state: link
notify: restart application
- name: Rollback logla
lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - ROLLBACK from {{ current_version.stdout }} to {{ rollback_version }}"
create: yes
handlers:
- name: restart application
systemd:
name: myapp
state: restarted
Paralel Deployment ve Performans Optimizasyonu
Çok sayıda sunucunuz varsa deployment süresini kısaltmak için paralel çalıştırma stratejileri kullanın:
# playbooks/webservers.yml - rolling update örneği
---
- name: Web Sunucuları Rolling Update
hosts: webservers
serial: "25%" # Bir seferde %25'ini güncelle
max_fail_percentage: 10 # %10'dan fazla fail olursa dur
become: yes
pre_tasks:
- name: Load balancer'dan çıkar
uri:
url: "http://lb.internal/api/servers/{{ inventory_hostname }}/disable"
method: POST
status_code: 200
delegate_to: localhost
- name: Aktif bağlantıların bitmesini bekle
wait_for:
host: "{{ ansible_host }}"
port: 8080
state: drained
timeout: 30
tasks:
- name: Uygulamayı güncelle
include_role:
name: app_deploy
vars:
deploy_version: "{{ app_version }}"
post_tasks:
- name: Health check
uri:
url: "http://{{ ansible_host }}:8080/health"
status_code: 200
retries: 5
delay: 10
- name: Load balancer'a geri ekle
uri:
url: "http://lb.internal/api/servers/{{ inventory_hostname }}/enable"
method: POST
status_code: 200
delegate_to: localhost
Pipeline Logları ve Audit Trail
Deployment’ları takip etmek için callback plugin’i veya basit bir loglama mekanizması kullanın:
# ansible.cfg'e ekle
[defaults]
log_path = /var/log/ansible/ansible.log
callbacks_enabled = timer, profile_tasks, log_plays
GitLab ve Jenkins’te deployment geçmişi zaten tutulduğu için, Ansible log’larını artifact olarak saklamak yeterli oluyor:
# GitLab CI artifact tanımı
deploy-to-production:
stage: deploy-production
script:
- ansible-playbook ... 2>&1 | tee deployment.log
artifacts:
paths:
- deployment.log
expire_in: 30 days
when: always
Yaygın Hatalar ve Çözümleri
SSH key permission sorunları: CI ortamında SSH key’i dosyaya yazarken rn satır sonu karakterleri sorun çıkarıyor.
# Doğru yöntem
echo "$SSH_PRIVATE_KEY" | tr -d 'r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
Vault password dosyası temizlenmeden kalıyor: after_script veya post bloğunda mutlaka temizleyin:
# GitLab CI
after_script:
- rm -f .vault_pass
# Jenkins
post {
always {
sh 'rm -f .vault_pass'
cleanWs()
}
}
Timeout sorunları: Uzun süren deployment’larda CI/CD sisteminin job’ı timeout ile öldürmesi. GitLab CI’da:
deploy-to-production:
timeout: 2 hours
script:
- ansible-playbook ...
Facts caching: Her pipeline run’da facts toplamak zaman alıyor. Cache kullanın:
# ansible.cfg
[defaults]
fact_caching = redis
fact_caching_connection = redis://localhost:6379
fact_caching_timeout = 3600
Sonuç
CI/CD pipeline ile Ansible entegrasyonu, başlangıçta biraz setup gerektiriyor ama uzun vadede kazanım inanılmaz büyük. Artık deployment’larınızın her birinde kimin ne zaman hangi versiyonu nereye kurduğunu tam olarak biliyorsunuz. Syntax hatası olan playbook canlıya çıkmıyor. Rolling update stratejisiyle downtime olmadan güncelleme yapabiliyorsunuz.
Pratik öneriler şunlar: Önce staging ortamında her şeyi otomatik, production’ı manual approval ile başlatın. Zamanla ekibinizin güveni arttıkça production’ı da otomasyona taşıyabilirsiniz. Rollback mekanizmasını kurmadan deployment otomasyonunu canlıya almayın, çünkü bir gün mutlaka lazım olacak. Molecule ile role testlerini pipeline’a eklemek ise code review kalitesini dramatik biçimde artırıyor.
En büyük tuzak, her şeyi mükemmel yapmaya çalışmak. Basit bir syntax check ve staging deployment ile başlayın, çalışır duruma getirin, sonra üzerine ekleyin. “Sonra yapacağım” dediğiniz manuel deployment’lar ne kadar uzun sürerse, otomasyon o kadar değerli oluyor.