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.

Bir yanıt yazın

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