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_filedeğişkenini inventory’de belirt veyassh-agentkullan - Caddyfile validate hatası: Template sözdizimini
caddy validate --config /etc/caddy/Caddyfileile 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 caddyile 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.