Caddy Web Sunucusunu Ansible ile Otomatik Olarak Dağıtın

Birden fazla sunucuya Caddy kurup yapılandırmak, her seferinde aynı adımları tekrar etmek… Eğer bunu elle yapıyorsan, er ya da geç bir hata yapacaksın veya sunucular arasında tutarsızlıklar çıkacak. Ansible ile bu süreci otomatize etmek hem zaman kazandırır hem de altyapını kod olarak yönetmenin kapısını açar. Bu yazıda sıfırdan başlayarak Caddy’yi Ansible ile nasıl dağıtacağını, yapılandıracağını ve yöneteceğini adım adım göstereceğim.

Neden Ansible ve Caddy Birlikteliği?

Caddy, otomatik HTTPS ve sade yapılandırma sözdizimi ile öne çıkan modern bir web sunucusu. Ansible ise agentless mimarisi ve YAML tabanlı playbook’larıyla altyapı otomasyonunun vazgeçilmezi. Bu ikisini birleştirdiğinde şunları elde ediyorsun:

  • Tekrarlanabilirlik: Aynı playbook’u 1 sunucuya da 100 sunucuya da uygulayabilirsin
  • İdempotency: Playbook’u birden fazla çalıştırdığında yan etki olmaz
  • Versiyon kontrolü: Caddy yapılandırman Git’te, değişiklik geçmişin var
  • Hızlı kurtarma: Sunucu çöktüğünde tek komutla aynı ortamı yeniden kurarsın

Gerçek dünya senaryosu olarak düşün: 5 farklı müşteri için 5 ayrı VPS’te Caddy ile reverse proxy ve statik site servisi yapıyorsun. Her ay bir iki yeni müşteri geliyor. Elle yapmak yerine Ansible ile bu işi dakikalar içinde halledersin.

Ortam Hazırlığı

Ansible Kurulumu

Kontrol makinende (kendi bilgisayarın veya CI/CD sunucusu) Ansible kurulu olmalı.

# Ubuntu/Debian
sudo apt update
sudo apt install ansible -y

# RHEL/CentOS/Rocky Linux
sudo dnf install ansible -y

# pip ile (her distro'da çalışır, önerilen)
pip3 install ansible

# Versiyon kontrolü
ansible --version

Hedef sunucularda ise sadece Python ve SSH erişimi yeterli. Ansible’ın agentless olmasının güzelliği bu.

Inventory Dosyası

Proje dizinini oluşturalım ve inventory’den başlayalım:

mkdir caddy-ansible && cd caddy-ansible

inventory/hosts.yml dosyasını oluştur:

cat > inventory/hosts.yml << 'EOF'
all:
  children:
    web_servers:
      hosts:
        web01:
          ansible_host: 192.168.1.10
          ansible_user: ubuntu
          caddy_server_name: web01.ornek.com
        web02:
          ansible_host: 192.168.1.11
          ansible_user: ubuntu
          caddy_server_name: web02.ornek.com
    staging:
      hosts:
        staging01:
          ansible_host: 192.168.1.20
          ansible_user: ubuntu
          caddy_server_name: staging.ornek.com
EOF

Bağlantıyı test et:

ansible all -i inventory/hosts.yml -m ping

Her şey yolundaysa yeşil pong cevapları görürsün.

Dizin Yapısı

Düzenli bir Ansible projesi için şu yapıyı kullanacağız:

caddy-ansible/
├── inventory/
│   └── hosts.yml
├── group_vars/
│   ├── all.yml
│   └── web_servers.yml
├── roles/
│   └── caddy/
│       ├── tasks/
│       │   └── main.yml
│       ├── templates/
│       │   ├── Caddyfile.j2
│       │   └── caddy.service.j2
│       ├── handlers/
│       │   └── main.yml
│       └── defaults/
│           └── main.yml
├── playbooks/
│   ├── install_caddy.yml
│   └── deploy_site.yml
└── site.yml

Bu yapıyı oluştur:

mkdir -p roles/caddy/{tasks,templates,handlers,defaults}
mkdir -p group_vars
mkdir -p playbooks

Değişkenlerin Tanımlanması

group_vars/all.yml

cat > group_vars/all.yml << 'EOF'
# Caddy versiyon ayarları
caddy_version: "2.7.6"
caddy_arch: "amd64"

# Dizin yapıları
caddy_home: "/etc/caddy"
caddy_data_dir: "/var/lib/caddy"
caddy_log_dir: "/var/log/caddy"
caddy_www_dir: "/var/www"

# Kullanıcı/grup
caddy_user: "caddy"
caddy_group: "caddy"

# ACME/Let's Encrypt
caddy_email: "[email protected]"
caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory"

# Genel ayarlar
caddy_http_port: 80
caddy_https_port: 443
EOF

group_vars/web_servers.yml

cat > group_vars/web_servers.yml << 'EOF'
# Production sunuculara özel ayarlar
caddy_sites:
  - name: "ornek.com"
    root: "/var/www/ornek"
    php_fpm: false
    reverse_proxy: false
  - name: "api.ornek.com"
    reverse_proxy: true
    upstream: "localhost:3000"
    php_fpm: false

# Rate limiting
caddy_rate_limit_enabled: true
caddy_rate_limit_requests: 100
caddy_rate_limit_window: "1m"
EOF

Caddy Role Oluşturma

Role Defaults

roles/caddy/defaults/main.yml:

cat > roles/caddy/defaults/main.yml << 'EOF'
caddy_plugins: []
caddy_sites: []
caddy_global_options: {}
caddy_admin_api_enabled: false
caddy_admin_listen: "localhost:2019"
caddy_log_level: "INFO"
caddy_auto_https: true
EOF

Görevler (Tasks)

roles/caddy/tasks/main.yml dosyası ana görev dosyamız. Bunu birkaç mantıksal bölüme ayıralım:

cat > roles/caddy/tasks/main.yml << 'EOF'
---
- name: Sistem paketlerini güncelle
  ansible.builtin.apt:
    update_cache: yes
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Gerekli bağımlılıkları kur
  ansible.builtin.package:
    name:
      - curl
      - gnupg
      - apt-transport-https
      - ca-certificates
    state: present
  when: ansible_os_family == "Debian"

- name: Caddy GPG anahtarını ekle
  ansible.builtin.apt_key:
    url: "https://dl.cloudsmith.io/public/caddy/stable/gpg.key"
    state: present
  when: ansible_os_family == "Debian"

- name: Caddy repository ekle
  ansible.builtin.apt_repository:
    repo: "deb https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main"
    state: present
    filename: caddy-stable
  when: ansible_os_family == "Debian"

- name: Caddy'yi kur
  ansible.builtin.package:
    name: caddy
    state: present
  notify: caddy restart

- name: Caddy kullanıcı ve grup kontrolü
  ansible.builtin.user:
    name: "{{ caddy_user }}"
    group: "{{ caddy_group }}"
    system: yes
    shell: /bin/false
    home: "{{ caddy_data_dir }}"
    create_home: no
  ignore_errors: yes

- name: Gerekli dizinleri oluştur
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: "{{ caddy_user }}"
    group: "{{ caddy_group }}"
    mode: '0755'
  loop:
    - "{{ caddy_home }}"
    - "{{ caddy_data_dir }}"
    - "{{ caddy_log_dir }}"
    - "{{ caddy_www_dir }}"

- name: Caddyfile'ı template'den oluştur
  ansible.builtin.template:
    src: Caddyfile.j2
    dest: "{{ caddy_home }}/Caddyfile"
    owner: "{{ caddy_user }}"
    group: "{{ caddy_group }}"
    mode: '0644'
    validate: "/usr/bin/caddy validate --config %s"
  notify: caddy reload

- name: Web sitesi dizinlerini oluştur
  ansible.builtin.file:
    path: "{{ caddy_www_dir }}/{{ item.name }}"
    state: directory
    owner: "{{ caddy_user }}"
    group: "www-data"
    mode: '0755'
  loop: "{{ caddy_sites }}"
  when: not item.reverse_proxy | default(false)

- name: Caddy servisini etkinleştir ve başlat
  ansible.builtin.systemd:
    name: caddy
    enabled: yes
    state: started
    daemon_reload: yes

- name: Firewall kurallarını yapılandır (ufw)
  community.general.ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - "80"
    - "443"
  when: ansible_os_family == "Debian"
EOF

Handler’lar

roles/caddy/handlers/main.yml:

cat > roles/caddy/handlers/main.yml << 'EOF'
---
- name: caddy restart
  ansible.builtin.systemd:
    name: caddy
    state: restarted

- name: caddy reload
  ansible.builtin.command:
    cmd: caddy reload --config /etc/caddy/Caddyfile
  register: caddy_reload_result
  failed_when: caddy_reload_result.rc != 0
EOF

Handler’ların önemini vurgulamak gerekiyor: Caddy, yapılandırma değişikliklerini sıcak yükleme (hot reload) destekliyor. caddy reload komutu kesintisiz yeniden yükleme yapıyor, bu yüzden restart yerine reload tercih ediyoruz.

Caddyfile Template

roles/caddy/templates/Caddyfile.j2 en kritik parça:

cat > roles/caddy/templates/Caddyfile.j2 << 'EOF'
# Bu dosya Ansible tarafından yönetilmektedir
# Değişikliklerinizi roles/caddy/templates/Caddyfile.j2 üzerinden yapın
# Son güncelleme: {{ ansible_date_time.iso8601 }}

{
    email {{ caddy_email }}
    
{% if not caddy_auto_https %}
    auto_https off
{% endif %}

{% if caddy_admin_api_enabled %}
    admin {{ caddy_admin_listen }}
{% else %}
    admin off
{% endif %}

    log {
        level {{ caddy_log_level }}
        output file {{ caddy_log_dir }}/caddy.log {
            roll_size 100mb
            roll_keep 5
        }
    }
}

{% for site in caddy_sites %}
{{ site.name }} {
{% if site.reverse_proxy is defined and site.reverse_proxy %}
    reverse_proxy {{ site.upstream }} {
        health_uri /health
        health_interval 30s
        health_timeout 10s
    }
{% else %}
    root * {{ caddy_www_dir }}/{{ site.name }}
    file_server
{% endif %}

{% if site.php_fpm is defined and site.php_fpm %}
    php_fastcgi unix//run/php/php{{ site.php_version | default('8.2') }}-fpm.sock
{% endif %}

    encode gzip zstd

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    log {
        output file {{ caddy_log_dir }}/{{ site.name }}.access.log {
            roll_size 50mb
            roll_keep 3
        }
    }
}

{% endfor %}
EOF

Playbook’lar

Ana Kurulum Playbook’u

playbooks/install_caddy.yml:

cat > playbooks/install_caddy.yml << 'EOF'
---
- name: Caddy Web Sunucusu Kurulumu
  hosts: web_servers
  become: yes
  gather_facts: yes

  pre_tasks:
    - name: Ansible bağlantısını doğrula
      ansible.builtin.ping:
      
    - name: Sistem bilgilerini göster
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} - {{ ansible_distribution }} {{ ansible_distribution_version }}"

  roles:
    - role: caddy

  post_tasks:
    - name: Caddy servis durumunu kontrol et
      ansible.builtin.command:
        cmd: systemctl is-active caddy
      register: caddy_status
      changed_when: false

    - name: Sonucu göster
      ansible.builtin.debug:
        msg: "Caddy durumu: {{ caddy_status.stdout }}"

    - name: Caddy versiyonunu doğrula
      ansible.builtin.command:
        cmd: caddy version
      register: installed_version
      changed_when: false

    - name: Versiyon bilgisi
      ansible.builtin.debug:
        msg: "Kurulu Caddy: {{ installed_version.stdout }}"
EOF

Site Dağıtım Playbook’u

playbooks/deploy_site.yml:

cat > playbooks/deploy_site.yml << 'EOF'
---
- name: Web Sitesi Dağıtımı
  hosts: "{{ target_hosts | default('web_servers') }}"
  become: yes
  gather_facts: yes

  vars:
    site_source: "{{ playbook_dir }}/../sites/{{ site_name }}"
    site_dest: "/var/www/{{ site_name }}"
    backup_enabled: true

  pre_tasks:
    - name: Site kaynağının var olduğunu kontrol et
      ansible.builtin.stat:
        path: "{{ site_source }}"
      register: site_check
      delegate_to: localhost

    - name: Site dizini bulunamadı hatası
      ansible.builtin.fail:
        msg: "Site dizini bulunamadı: {{ site_source }}"
      when: not site_check.stat.exists

  tasks:
    - name: Mevcut siteyi yedekle
      ansible.builtin.archive:
        path: "{{ site_dest }}"
        dest: "/tmp/{{ site_name }}_backup_{{ ansible_date_time.epoch }}.tar.gz"
        format: gz
      when: backup_enabled
      ignore_errors: yes

    - name: Site dosyalarını senkronize et
      ansible.posix.synchronize:
        src: "{{ site_source }}/"
        dest: "{{ site_dest }}/"
        delete: yes
        recursive: yes
        rsync_opts:
          - "--exclude=.git"
          - "--exclude=node_modules"
          - "--exclude=*.log"

    - name: Dosya izinlerini düzenle
      ansible.builtin.file:
        path: "{{ site_dest }}"
        owner: caddy
        group: www-data
        mode: '0755'
        recurse: yes

    - name: Caddy yapılandırmasını güncelle
      ansible.builtin.template:
        src: "{{ playbook_dir }}/../roles/caddy/templates/Caddyfile.j2"
        dest: /etc/caddy/Caddyfile
        validate: "/usr/bin/caddy validate --config %s"
      notify: Caddy yeniden yükle

  handlers:
    - name: Caddy yeniden yükle
      ansible.builtin.command:
        cmd: caddy reload --config /etc/caddy/Caddyfile
EOF

Gerçek Dünya Senaryoları

Senaryo 1: Staging’den Production’a Terfi

CI/CD pipeline’ında önce staging’e dağıtım yapıp test edersin, sonra production’a alırsın:

# Staging'e dağıtım
ansible-playbook -i inventory/hosts.yml playbooks/deploy_site.yml 
  -e "site_name=ornek.com" 
  -e "target_hosts=staging" 
  --tags deploy

# Staging testleri geçtikten sonra production
ansible-playbook -i inventory/hosts.yml playbooks/deploy_site.yml 
  -e "site_name=ornek.com" 
  -e "target_hosts=web_servers" 
  --tags deploy

Senaryo 2: Sertifika Yenileme Kontrolü

Caddy otomatik sertifika yenileme yapıyor ama bunu Ansible ile periyodik olarak kontrol etmek iyi bir pratik:

cat > playbooks/check_certificates.yml << 'EOF'
---
- name: SSL Sertifika Kontrolü
  hosts: web_servers
  become: yes
  gather_facts: no

  tasks:
    - name: Sertifika bilgilerini al
      ansible.builtin.shell:
        cmd: |
          for domain in $(caddy list-modules 2>/dev/null | grep tls || echo ""); do
            echo $domain
          done
          find /var/lib/caddy/.local/share/caddy/certificates -name "*.crt" -exec 
          openssl x509 -enddate -noout -in {} ; 2>/dev/null
      register: cert_info
      changed_when: false

    - name: Sertifika bilgilerini göster
      ansible.builtin.debug:
        msg: "{{ cert_info.stdout_lines }}"
EOF

Senaryo 3: Acil Rollback

Bir şeyler ters giderse hızlıca bir önceki yapılandırmaya dön:

cat > playbooks/rollback.yml << 'EOF'
---
- name: Caddy Yapılandırma Rollback
  hosts: "{{ target_hosts | default('web_servers') }}"
  become: yes

  tasks:
    - name: Yedek yapılandırma listesi
      ansible.builtin.find:
        paths: /etc/caddy/
        patterns: "Caddyfile.*.bak"
      register: backup_files

    - name: En son yedeği bul
      ansible.builtin.set_fact:
        latest_backup: "{{ backup_files.files | sort(attribute='mtime') | last }}"
      when: backup_files.files | length > 0

    - name: Yedekten geri yükle
      ansible.builtin.copy:
        src: "{{ latest_backup.path }}"
        dest: /etc/caddy/Caddyfile
        remote_src: yes
        backup: yes
      when: backup_files.files | length > 0

    - name: Caddy'yi yeniden yükle
      ansible.builtin.command:
        cmd: caddy reload --config /etc/caddy/Caddyfile
      when: backup_files.files | length > 0
EOF

Tags Kullanımı ile Kısmi Çalıştırma

Büyük playbook’larda sadece belirli adımları çalıştırmak isteyebilirsin. Tags bunu sağlar:

# Sadece yapılandırma dosyalarını güncelle
ansible-playbook -i inventory/hosts.yml site.yml --tags config

# Sadece kurulum adımlarını çalıştır
ansible-playbook -i inventory/hosts.yml site.yml --tags install

# Belirli bir adımı atla
ansible-playbook -i inventory/hosts.yml site.yml --skip-tags backup

# Dry-run (ne yapılacağını gör, uygulama)
ansible-playbook -i inventory/hosts.yml site.yml --check --diff

Vault ile Hassas Verilerin Korunması

API anahtarları, sertifika şifreleri gibi hassas verileri düz YAML’da tutmamalısın:

# Vault şifresi oluştur
echo "cok-guclu-sifre-buraya" > ~/.vault_pass
chmod 600 ~/.vault_pass

# Vault ile şifreli değişken dosyası oluştur
ansible-vault create group_vars/all/vault.yml --vault-password-file ~/.vault_pass

# İçeriğe şunu ekle:
# vault_caddy_email: "[email protected]"
# vault_acme_account_key: "..."

# Mevcut dosyayı şifrele
ansible-vault encrypt group_vars/web_servers.yml --vault-password-file ~/.vault_pass

# Playbook'u vault şifresiyle çalıştır
ansible-playbook -i inventory/hosts.yml site.yml --vault-password-file ~/.vault_pass

Idempotency Testi

Ansible’ın idempotency özelliğinin gerçekten çalıştığını doğrulamak için playbook’u iki kez çalıştır ve changed sayısının ikinci çalıştırmada sıfır olmasını bekle:

# İlk çalıştırma
ansible-playbook -i inventory/hosts.yml playbooks/install_caddy.yml

# Çıktıda "changed=X" değerini not al

# İkinci çalıştırma - changed=0 olmalı
ansible-playbook -i inventory/hosts.yml playbooks/install_caddy.yml

# Verbose modda ne yapıldığını gör
ansible-playbook -i inventory/hosts.yml playbooks/install_caddy.yml -v

Monitoring Entegrasyonu

Dağıtım sonrası Caddy’nin sağlık durumunu kontrol eden basit bir görev:

cat >> roles/caddy/tasks/main.yml << 'EOF'

- name: Caddy admin API'yi kontrol et
  ansible.builtin.uri:
    url: "http://localhost:2019/config/"
    method: GET
    status_code: 200
  register: api_check
  retries: 3
  delay: 5
  when: caddy_admin_api_enabled
  ignore_errors: yes

- name: HTTP yanıt kontrolü
  ansible.builtin.uri:
    url: "http://localhost"
    method: GET
    status_code:
      - 200
      - 301
      - 302
  register: http_check
  retries: 3
  delay: 5
  ignore_errors: yes

- name: Servis durumu özeti
  ansible.builtin.debug:
    msg: |
      Caddy durumu: {{ 'OK' if http_check.status in [200, 301, 302] else 'HATA' }}
      Admin API: {{ 'Aktif' if caddy_admin_api_enabled else 'Devre dışı' }}
EOF

CI/CD Pipeline ile Entegrasyon

GitHub Actions veya GitLab CI ile otomatik dağıtım için örnek bir yapı:

# .github/workflows/deploy.yml olarak kaydet
cat > deploy-workflow-example.yml << 'EOF'
name: Caddy Dağıtım

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Ansible kur
        run: pip install ansible
      
      - name: SSH anahtarını yapılandır
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
      
      - name: Syntax kontrolü
        run: |
          ansible-playbook -i inventory/hosts.yml site.yml --syntax-check
      
      - name: Dağıtımı yap
        run: |
          ansible-playbook -i inventory/hosts.yml site.yml 
            --vault-password-file <(echo "${{ secrets.VAULT_PASS }}")
        env:
          ANSIBLE_HOST_KEY_CHECKING: "False"
EOF

Sık Karşılaşılan Sorunlar ve Çözümleri

Playbook çalıştırırken karşılaşabileceğin tipik problemler:

  • Permission denied SSH hatası: ansible_ssh_private_key_file değişkenini inventory’de belirt veya ssh-agent kullan
  • Caddyfile validate hatası: Template sözdizimini caddy validate --config /etc/caddy/Caddyfile ile manuel test et
  • Port 80/443 kullanımda hatası: netstat -tlnp | grep -E ':80|:443' ile hangi servisin portu tuttuğunu bul
  • Caddy servis başlamıyor: journalctl -xeu caddy ile detaylı loglara bak
  • Let’s Encrypt rate limit: Geliştirme ortamında caddy_acme_ca: "https://acme-staging-v02.api.letsencrypt.org/directory" kullan

Sonuç

Caddy ve Ansible kombinasyonu, web sunucu altyapını profesyonelce yönetmenin en pratik yollarından biri. Bu yazıda ele aldığımız yaklaşımla:

  • Sıfırdan Ansible role yapısı kurduk
  • Jinja2 template’lerle dinamik Caddyfile üretimini sağladık
  • Handler’larla kesintisiz reload yönetimini implemente ettik
  • Staging/production ayrımını inventory ile çözdük
  • Rollback senaryosu için hazır playbook yazdık
  • Vault ile hassas verilerin güvenliğini sağladık

Başlangıç için fazla karmaşık görünebilir ama bir kez kurduğunda yeni sunucu eklemek tek satır inventory girişine indirgeliyor. 10 sunucun olduğunda bu yaklaşımın değerini çok daha net görürsün.

Bir sonraki adım olarak Ansible Galaxy’deki hazır Caddy role’lerini inceleyebilirsin (ansible-galaxy search caddy), ya da bu yapıyı Terraform ile birleştirerek tam bir Infrastructure as Code çözümüne geçebilirsin. Altyapını kod olarak yönetmeye başladıktan sonra geri dönmek istemiyorsun, bunu garanti ederim.

Yorum yapın