Docker Konteyner Dağıtımı: Ansible ile Tam Rehber
Konteyner altyapısı kurarken “elle mi yapayım, Ansible mı yazayım?” sorusuyla uğraşmayan sysadmin yok gibi. Küçük ortamlarda Docker komutlarını tek tek çalıştırmak idare eder, ama 10-20 sunucuya aynı deployment’ı yaymak gerekince iş çığırından çıkıyor. Ansible burada devreye giriyor: idempotent yapısıyla, YAML tabanlı playbook’larıyla ve SSH üzerinden çalışmasıyla Docker deployment’larını hem tekrarlanabilir hem de okunabilir hale getiriyor. Bu yazıda gerçek dünya senaryolarıyla, production ortamında kullanabileceğin Ansible playbook’ları yazacağız.
Neden Ansible + Docker Kombinasyonu?
Shell script’le Docker deployment yapmak mümkün, ama bakımı cehennem. Bir değişken yanlış gittiğinde script’in neyi yaptığını çözmek için saatlerce log okursun. Ansible’ın getirdiği en büyük avantaj idempotency: aynı playbook’u 10 kez çalıştırsan da sonuç değişmez, sistem istenen duruma getirilir.
Birkaç somut avantaj sıralayayım:
- Tekrar çalıştırılabilirlik: Konteyner zaten ayaktaysa playbook onu yeniden başlatmaz
- Değişken yönetimi: Farklı ortamlar için (dev, staging, prod) ayrı variable dosyaları kullanabilirsin
- Rol tabanlı yapı: Docker kurulumu, network ayarları, uygulama deployment’ı gibi sorumlulukları rollere bölebilirsin
- Vault entegrasyonu: Registry şifreleri, API anahtarları güvenle saklanır
Ortam Hazırlığı ve Envanter Yapısı
Önce proje dizin yapısını kuralım. Production ortamında işe yarayan bir yapı şu şekilde:
ansible-docker-deploy/
├── inventories/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ ├── all.yml
│ │ └── docker_hosts.yml
│ └── staging/
│ ├── hosts.yml
│ └── group_vars/
│ └── docker_hosts.yml
├── roles/
│ ├── docker_install/
│ ├── docker_network/
│ └── app_deploy/
├── playbooks/
│ ├── install_docker.yml
│ └── deploy_app.yml
└── ansible.cfg
Envanter dosyasını hazırlayalım:
# inventories/production/hosts.yml
all:
children:
docker_hosts:
hosts:
web01:
ansible_host: 192.168.1.10
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/prod_key
web02:
ansible_host: 192.168.1.11
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/prod_key
db_hosts:
hosts:
db01:
ansible_host: 192.168.1.20
ansible_user: ubuntu
ansible.cfg dosyasını da düzenleyelim:
# ansible.cfg
[defaults]
inventory = inventories/production/hosts.yml
remote_user = ubuntu
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
roles_path = roles/
[privilege_escalation]
become = True
become_method = sudo
become_user = root
Docker Kurulum Rolü
İlk rolümüzü yazalım. Docker kurulumunu idempotent yapmanın püf noktası, önce kurulu olup olmadığını kontrol etmek.
# roles/docker_install/tasks/main.yml
---
- name: Gerekli paketleri kur
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-pip
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Docker GPG anahtarini ekle
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Docker repository ekle
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
filename: docker
- name: Docker Engine kur
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
- name: Docker servisini baslat ve enable et
systemd:
name: docker
state: started
enabled: yes
- name: deploy kullanicisini docker grubuna ekle
user:
name: "{{ ansible_user }}"
groups: docker
append: yes
- name: Docker Python SDK kur
pip:
name:
- docker
- docker-compose
executable: pip3
Bu rolü çalıştırmak için basit bir playbook:
# playbooks/install_docker.yml
---
- name: Docker kurulumu
hosts: docker_hosts
become: yes
roles:
- docker_install
post_tasks:
- name: Docker versiyonunu kontrol et
command: docker --version
register: docker_version
changed_when: false
- name: Docker versiyon bilgisi goster
debug:
msg: "Kurulan Docker: {{ docker_version.stdout }}"
Uygulama Deployment Rolü
Asıl eğlenceli kısma geldik. Gerçek dünyada bir web uygulamasını containerize edip deploy etmek için kapsamlı bir rol yazalım.
Önce değişkenleri tanımlayalım:
# inventories/production/group_vars/docker_hosts.yml
---
# Registry ayarlari
docker_registry: "registry.sirket.com"
docker_registry_user: "deploy-user"
# Vault ile sifrelenmis olacak
docker_registry_password: "{{ vault_registry_password }}"
# Uygulama ayarlari
app_name: "mywebapp"
app_image: "registry.sirket.com/mywebapp"
app_tag: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
app_port: 8080
app_replicas: 1
# Container kaynak limitleri
container_memory_limit: "512m"
container_cpu_limit: "0.5"
# Volume ayarlari
app_data_dir: "/opt/mywebapp/data"
app_log_dir: "/var/log/mywebapp"
# Network
app_network: "webapp_network"
# Ortam degiskenleri
app_env_vars:
NODE_ENV: "production"
DB_HOST: "{{ db_host }}"
DB_PORT: "5432"
LOG_LEVEL: "info"
APP_PORT: "{{ app_port }}"
Deployment rolünün ana task dosyası:
# roles/app_deploy/tasks/main.yml
---
- name: Gerekli dizinleri olustur
file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: docker
mode: '0755'
loop:
- "{{ app_data_dir }}"
- "{{ app_log_dir }}"
- "/opt/{{ app_name }}"
- name: Docker network olustur
docker_network:
name: "{{ app_network }}"
state: present
- name: Private registry'e login ol
docker_login:
registry_url: "{{ docker_registry }}"
username: "{{ docker_registry_user }}"
password: "{{ docker_registry_password }}"
reauthorize: yes
no_log: true
- name: Eski image'i cek (pre-pull for zero downtime)
docker_image:
name: "{{ app_image }}"
tag: "{{ app_tag }}"
source: pull
force_source: yes
- name: Eski konteyneri durdur (graceful shutdown)
docker_container:
name: "{{ app_name }}"
state: stopped
ignore_errors: yes
- name: Konteyneri deploy et
docker_container:
name: "{{ app_name }}"
image: "{{ app_image }}:{{ app_tag }}"
state: started
restart_policy: unless-stopped
network_mode: "{{ app_network }}"
ports:
- "{{ app_port }}:{{ app_port }}"
volumes:
- "{{ app_data_dir }}:/app/data"
- "{{ app_log_dir }}:/app/logs"
env: "{{ app_env_vars }}"
memory: "{{ container_memory_limit }}"
cpus: "{{ container_cpu_limit }}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
app: "{{ app_name }}"
version: "{{ app_tag }}"
managed_by: "ansible"
- name: Konteynerin sagligi kontrol et
command: docker inspect --format='{{ "{{" }}.State.Health.Status{{ "}}" }}' {{ app_name }}
register: health_status
until: health_status.stdout == "healthy"
retries: 10
delay: 15
changed_when: false
- name: Eski image'lari temizle
docker_prune:
images: yes
images_filters:
dangling: true
Docker Compose ile Çoklu Servis Deployment
Tek konteyner yetmez, çoğunlukla birden fazla servis birlikte çalışır. Ansible ile docker_compose modülünü kullanarak stack deployment yapabiliriz.
# roles/app_deploy/tasks/compose_deploy.yml
---
- name: Docker Compose dosyasini kopyala
template:
src: docker-compose.yml.j2
dest: "/opt/{{ app_name }}/docker-compose.yml"
owner: "{{ ansible_user }}"
group: docker
mode: '0640'
register: compose_file_changed
- name: .env dosyasini olustur
template:
src: env.j2
dest: "/opt/{{ app_name }}/.env"
owner: "{{ ansible_user }}"
group: docker
mode: '0600'
no_log: true
- name: Compose stack'i deploy et
community.docker.docker_compose_v2:
project_src: "/opt/{{ app_name }}"
state: present
pull: always
register: compose_result
- name: Deploy sonucunu goster
debug:
var: compose_result
when: compose_result.changed
Compose template dosyası:
# roles/app_deploy/templates/docker-compose.yml.j2
version: '3.8'
services:
app:
image: {{ app_image }}:{{ app_tag }}
container_name: {{ app_name }}_app
restart: unless-stopped
environment:
{% for key, value in app_env_vars.items() %}
- {{ key }}={{ value }}
{% endfor %}
ports:
- "{{ app_port }}:{{ app_port }}"
volumes:
- app_data:{{ app_container_data_path | default('/app/data') }}
- {{ app_log_dir }}:/app/logs
networks:
- {{ app_network }}
depends_on:
redis:
condition: service_healthy
deploy:
resources:
limits:
memory: {{ container_memory_limit }}
cpus: '{{ container_cpu_limit }}'
redis:
image: redis:7-alpine
container_name: {{ app_name }}_redis
restart: unless-stopped
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- {{ app_network }}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
container_name: {{ app_name }}_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/ssl/certs:/etc/ssl/certs:ro
networks:
- {{ app_network }}
depends_on:
- app
networks:
{{ app_network }}:
external: false
volumes:
app_data:
redis_data:
Rolling Update Stratejisi
Birden fazla sunucuya sıralı deployment yapmanın püf noktası Ansible’ın serial özelliği. Aşağıdaki playbook bir anda tek sunucuya deploy yapar, sağlık kontrolü geçerse devam eder:
# playbooks/rolling_deploy.yml
---
- name: Rolling deployment - Load balancer'dan cikar
hosts: load_balancers
become: yes
tasks:
- name: Web sunucusunu LB'den cikar
haproxy:
state: disabled
host: "{{ item }}"
socket: /var/run/haproxy/admin.sock
loop: "{{ groups['docker_hosts'] }}"
when: "'haproxy' in ansible_run_tags"
- name: Rolling deployment - Uygulama guncelle
hosts: docker_hosts
become: yes
serial: 1
max_fail_percentage: 0
vars:
app_tag: "{{ new_version | mandatory }}"
roles:
- app_deploy
post_tasks:
- name: Uygulama ayakta mi kontrol et
uri:
url: "http://localhost:{{ app_port }}/health"
method: GET
status_code: 200
timeout: 30
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
- name: Hata durumunda rollback yap
block:
- name: Rollback bildir
debug:
msg: "HATA: {{ inventory_hostname }} saglik kontrolunden gecemedi, rollback yapiliyor"
- name: Onceki versiyona don
docker_container:
name: "{{ app_name }}"
image: "{{ app_image }}:{{ previous_version }}"
state: started
restart_policy: unless-stopped
when: health_check.status != 200
- name: Rolling deployment - Load balancer'a geri ekle
hosts: load_balancers
become: yes
tasks:
- name: Web sunucusunu LB'ye ekle
haproxy:
state: enabled
host: "{{ item }}"
socket: /var/run/haproxy/admin.sock
loop: "{{ groups['docker_hosts'] }}"
when: "'haproxy' in ansible_run_tags"
Rolling deployment’ı şöyle çalıştırırsın:
# Yeni versiyonu deploy et
ansible-playbook playbooks/rolling_deploy.yml
-e "new_version=v2.1.0"
-e "previous_version=v2.0.5"
--vault-password-file ~/.vault_pass
-i inventories/production/hosts.yml
# Sadece staging'e deploy et
ansible-playbook playbooks/rolling_deploy.yml
-e "new_version=v2.1.0"
-i inventories/staging/hosts.yml
--limit staging_web
# Dry-run ile ne yapacagini gor
ansible-playbook playbooks/rolling_deploy.yml
-e "new_version=v2.1.0"
--check --diff
Ansible Vault ile Gizli Bilgi Yönetimi
Registry şifreleri, veritabanı bağlantı bilgileri gibi hassas veriler playbook’larda açık durmamalı.
# Vault dosyasi olustur
ansible-vault create inventories/production/group_vars/vault.yml
# Mevcut dosyayi sifreleme
ansible-vault encrypt inventories/production/group_vars/vault.yml
# Duzenleme icin
ansible-vault edit inventories/production/group_vars/vault.yml
Vault dosyası içeriği:
# inventories/production/group_vars/vault.yml (sifrelenmis)
---
vault_registry_password: "super_gizli_sifre_buraya"
vault_db_password: "veritabani_sifresi"
vault_app_secret_key: "uygulama_gizli_anahtari"
vault_redis_password: "redis_sifresi"
Normal değişken dosyasında vault değişkenlerine referans verirsin:
# inventories/production/group_vars/docker_hosts.yml
---
docker_registry_password: "{{ vault_registry_password }}"
app_env_vars:
DATABASE_URL: "postgresql://app:{{ vault_db_password }}@{{ db_host }}/appdb"
SECRET_KEY: "{{ vault_app_secret_key }}"
REDIS_URL: "redis://:{{ vault_redis_password }}@redis:6379/0"
CI/CD Pipeline Entegrasyonu
GitLab CI ile bu playbook’ları otomatik tetikleyelim. .gitlab-ci.yml örneği:
# .gitlab-ci.yml
stages:
- build
- test
- deploy-staging
- deploy-production
variables:
APP_IMAGE: "$CI_REGISTRY_IMAGE"
APP_TAG: "$CI_COMMIT_SHORT_SHA"
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $APP_IMAGE:$APP_TAG .
- docker push $APP_IMAGE:$APP_TAG
deploy-staging:
stage: deploy-staging
image: cytopia/ansible:latest
environment:
name: staging
before_script:
- echo "$ANSIBLE_VAULT_PASS" > /tmp/.vault_pass
- echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
script:
- ansible-playbook playbooks/deploy_app.yml
-i inventories/staging/hosts.yml
-e "app_tag=$APP_TAG"
--vault-password-file /tmp/.vault_pass
--private-key /tmp/deploy_key
after_script:
- rm -f /tmp/.vault_pass /tmp/deploy_key
only:
- develop
deploy-production:
stage: deploy-production
image: cytopia/ansible:latest
environment:
name: production
before_script:
- echo "$ANSIBLE_VAULT_PASS" > /tmp/.vault_pass
- echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
script:
- ansible-playbook playbooks/rolling_deploy.yml
-i inventories/production/hosts.yml
-e "new_version=$APP_TAG"
-e "previous_version=$PREVIOUS_STABLE_TAG"
--vault-password-file /tmp/.vault_pass
--private-key /tmp/deploy_key
after_script:
- rm -f /tmp/.vault_pass /tmp/deploy_key
when: manual
only:
- main
Sık Karşılaşılan Sorunlar ve Çözümleri
Docker socket izin hatası: Ansible’ın Docker modülleri socket üzerinden iletişim kurar. Eğer Got permission denied while trying to connect to the Docker daemon socket hatası alıyorsan:
# Kullaniciyi docker grubuna ekle
- name: Kullaniciyi docker grubuna ekle
user:
name: "{{ ansible_user }}"
groups: docker
append: yes
# Yeni grup uygulamasi icin connection'i resetle
- name: SSH baglantisini yenile
meta: reset_connection
Image çekme zaman aşımı: Büyük image’larda timeout sorunu yaşanabilir:
# docker_image modulu icin timeout artir
- name: Buyuk image cek
docker_image:
name: "{{ app_image }}"
tag: "{{ app_tag }}"
source: pull
timeout: 300
Konteyner değişken önbellek sorunu: Aynı isimde konteyner varsa bazı parametreler güncellenmeyebilir. force_kill: yes veya önce state: absent yapıp sonra yeniden başlatmak çözüm olabilir.
Python Docker SDK eksikliği: community.docker koleksiyonu için hedef sistemde Python Docker SDK gerekli. Rol başına şu task’ı ekle:
- name: Docker SDK kur
pip:
name: docker
executable: pip3
state: present
become: yes
Monitoring ve Log Takibi
Deployment sonrası konteyner durumunu izlemek için basit bir monitoring task’ı:
# playbooks/check_containers.yml
---
- name: Konteyner saglik kontrolu
hosts: docker_hosts
become: yes
tasks:
- name: Calisan konteynerleri listele
command: docker ps --format "table {{ '{{' }}.Names{{ '}}' }}t{{ '{{' }}.Status{{ '}}' }}t{{ '{{' }}.Ports{{ '}}' }}"
register: running_containers
changed_when: false
- name: Konteyner listesini goster
debug:
msg: "{{ running_containers.stdout_lines }}"
- name: Unhealthy konteynerleri bul
shell: docker ps --filter health=unhealthy --format "{{ '{{' }}.Names{{ '}}' }}"
register: unhealthy_containers
changed_when: false
- name: Unhealthy konteyner varsa uyar
fail:
msg: "KRITIK: Sagliksiz konteynerlere: {{ unhealthy_containers.stdout }}"
when: unhealthy_containers.stdout != ""
- name: Son 100 log satirini al
command: docker logs --tail 100 {{ app_name }}
register: app_logs
changed_when: false
failed_when: false
- name: Loglari kaydet
copy:
content: "{{ app_logs.stdout }}"
dest: "/tmp/{{ app_name }}_deploy_logs_{{ ansible_date_time.epoch }}.txt"
delegate_to: localhost
Sonuç
Ansible ile Docker deployment otomasyonu, başlangıçta biraz kurulum gerektiriyor ama getirisi fazlasıyla hissediliyor. Özetlemek gerekirse:
- Rol tabanlı yapı kodu modüler ve yeniden kullanılabilir tutar
- Vault entegrasyonu hassas verileri güvende tutar
- Serial deployment sıfır kesinti süresiyle güncelleme sağlar
- CI/CD entegrasyonu insan müdahalesini minimuma indirir
- Idempotency sistemi her zaman istenen durumda tutar
Küçük bir ekipse, başlamak için production/staging ayrımı olan bir envanter yapısı ve temel bir app_deploy rolü yeterli. Zamanla rolleri zenginleştir, vault kullanımını genişlet, CI/CD pipeline’ına bağla. En önemlisi, her şeyi bir anda yapmaya çalışma. Mevcut manuel süreçlerini birer birer Ansible’a taşı, her adımda sistemi test et. Birkaç hafta içinde altyapı yönetiminin ne kadar rahatladığını göreceksin.
