İçeriğe geç
KURUMSAL Kurumsal teknoloji · İşletmenize özel mühendislik Kurumsal Yazılım · Yönetilen Sunucular · Kurumsal E-posta · LiteSpeed + CloudLinux · Imunify360 Güvenlik · cPanel Yönetim · Uygun pakette 3 Gbps'ye kadar ağ profili
AIOR

Ansible Rehberi: Inventory, Playbook, Rol ve Vault ile Sunucu Yönetimi

  • Konu başlatıcısı Konu başlatıcısı Aior
  • Başlangıç tarihi Başlangıç tarihi

Aior

Administrator

Yetkili üye
Katıldı
2 Nis 2023
Mesajlar
929
Tepki puanı
2
Puan
18
Yaş
41
Konum
Turkey
Web sitesi
aior.com
Konu sahibi

Ansible, sunuculara ajan (agent) kurmadan çalışan bir yapılandırma yönetimi (configuration management) aracıdır: sunucularınızın olması gereken durumunu YAML ile tarif edersiniz, Ansible SSH ile bağlanır ve yalnızca farklı olanı değiştirir. Bu rehberde AlmaLinux ve Ubuntu sunuculardan oluşan küçük, karma bir filo için baştan sona çalışan bir düzen kuruyoruz: kurulum, envanter, ilk gerçek playbook, roller, Ansible Vault, CI, hız ayarları ve sorun giderme. Komutlar ve sürümler Ekim 2026'da ansible-core 2.21 ve Ansible 14 paketiyle kontrol edildi, örnek playbook'lar da --syntax-check ve ansible-lint denetiminden geçiyor. Yine de üretimden önce bir test sunucusunda deneyin.

Ansible nedir, bir çalıştırma nasıl işler?​

Ansible tek bir makinede, kontrol düğümünde (control node) çalışır; bu bir dizüstü bilgisayar, bastion sunucusu ya da CI çalıştırıcısı olabilir. Değişiklikleri yönetilen düğümlere (managed node) kendisi gönderir (push) ve onlara sürekli çalışan bir servis kurmaz. Her görev için sistemdeki OpenSSH istemcisiyle bağlanır, küçük bir Python programını (modülü) sunucuya kopyalar, çalıştırır, JSON sonucunu okur ve dosyayı siler. Yönetilen düğümde SSH erişimi ve ansible-core 2.21 için Python 3.9-3.14 yeterlidir; AlmaLinux 9 ve 10 ile Ubuntu 22.04 ve 24.04 bu aralıkta bir Python ile gelir. AlmaLinux 8 ise istisna: dnf'in Python kütüphanesi (python3-dnf) yalnızca sistemdeki Python 3.6 için paketlenmiş, ansible-core ise 2.17'den beri yönetilen düğümde Python 3.6'yı desteklemiyor; yani yeni bir Python kurmak paket yönetimini çalıştırmaya yetmez. EL8 sunucuları ayrı ve daha eski bir ansible-core ortamından (upstream desteği bitmiş 2.16) yönetin ya da AlmaLinux 9 veya 10'a geçişlerini planlayın.

Modüllerin çoğu idempotent çalışır, yani tekrar tekrar çalıştırıldığında aynı sonucu verir: mevcut durumu istenen durumla karşılaştırır ve yalnızca fark varsa işlem yapar. Doğru yazılmış bir playbook'u ikinci kez çalıştırdığınızda görevler changed yerine ok döner. Tekrar çalıştırmayı güvenli kılan ve Ansible'ı bir klasör dolusu shell betiğinden ayıran asıl özellik budur. Bunun istisnası command ve shell modülleridir, çünkü Ansible herhangi bir komutun neyi değiştirdiğini bilemez.

KavramAnlamı
Kontrol düğümü (control node)Ansible'ın çalıştığı makine: dizüstü, bastion sunucusu, CI çalıştırıcısı
Yönetilen düğüm (managed node)Ansible'ın yapılandırdığı sunucu (SSH + Python)
Envanter (inventory)Sunucular, gruplar ve bunlara ait değişkenler
ModülTek bir iş birimi, örneğin ansible.builtin.user
Görev (task) / play / playbookTek modül çağrısı / bir sunucu grubu için görevler / play'lerden oluşan YAML dosyası
HandlerYalnızca bir değişiklik bildirildiğinde çalışan görev
Rol (role)Görev, handler, şablon ve varsayılan değerlerden oluşan yeniden kullanılabilir paket
Koleksiyon (collection)Modül, eklenti ve rol paketi, örneğin community.general
Fact'lerHer sunucudan toplanan bilgiler: işletim sistemi ailesi, IP adresleri, bellek

Ansible'ın yeri: Terraform ya da OpenTofu sunucuları, ağları ve DNS kayıtlarını oluşturur; Ansible ise sunucuların içinde çalışan yazılımı yapılandırır. Birçok ekip ikisini birlikte kullanır. Bu seçimi Terraform, Pulumi ve Ansible karşılaştırmamızda ele aldık, burada tekrarlamıyoruz.

Kontrol düğümüne Ansible kurulumu​

Kontrol düğümü Linux, macOS ya da bir BSD olabilir; Windows doğrudan kontrol düğümü olamaz. Öğrenmek için WSL iş görür; ancak Ansible belgelerindeki Windows rehberine göre WSL desteklenmez ve üretim sistemlerinde kullanılmamalıdır. WSL içinde projeyi /mnt/c altında değil, Linux ev dizininizde (~/) tutun: Ansible, herkesin yazabildiği (world-writable) görünen bir dizindeki ansible.cfg dosyasını yok sayar.

ansible-core, ansible.builtin modüllerini içeren motordur; ansible paketi buna ansible.posix ve community.general gibi seçilmiş koleksiyonları ekler (Ansible 14, ansible-core 2.21 üzerine kuruludur). ansible-core 2.21 kontrol düğümünde Python 3.12-3.14 ister. Sonraki sürüm 2.22 Kasım 2026 için planlanıyor ve Python 3.13 veya üstünü gerektiriyor (PyPI üzerindeki sürüm adayı bunu şimdiden zorunlu tutuyor). Bu yüzden Python 3.12 kullanan bir kontrol düğümü, upstream bakımı Kasım 2027'ye kadar süren 2.21.x serisinde kalır. pipx, Ansible'ı kendi sanal ortamına (virtual environment) kurar ve sistem Python'una dokunmaz:

Bash:
# Ubuntu 24.04 (Python 3.12)
sudo apt update && sudo apt install -y pipx
pipx ensurepath                      # ardından yeni bir terminal açın
pipx install --include-deps ansible

# AlmaLinux 9 (sistem Python'u 3.9, bu yüzden 3.12 paketini kullanın)
sudo dnf install -y python3.12 python3.12-pip
python3.12 -m pip install --user pipx
python3.12 -m pipx ensurepath
pipx install --include-deps ansible

# macOS
brew install pipx && pipx ensurepath
pipx install --include-deps ansible

ansible --version                    # ilk satır: ansible [core 2.21.x]
pipx upgrade --include-injected ansible

--include-deps şart, çünkü komutlar ansible-core bağımlılığından gelir; bu seçenek olmadan pipx, pakete bağlı uygulama bulunmadığını ("No apps associated with package") bildirir. Yalnızca motoru istiyorsanız ansible-core kurup gereken koleksiyonları bir requirements.yml dosyasından yükleyin (aşağıda roller bölümünde). Dağıtım paketleri işletim sistemiyle birlikte güncellenir ama sürüm olarak geriden gelir:

Kaynak (Ekim 2026'da kontrol edildi)Sürüm
Ubuntu 24.04, aptansible 9.2.0, ansible-core 2.16.3 üzerinde
Ubuntu 26.04, aptansible 13.1.0, ansible-core 2.20.1 üzerinde
Ansible PPA (ppa:ansible/ansible), Ubuntu 24.04ansible 14.5.0, ansible-core 2.21.5 üzerinde
AlmaLinux 9, AppStreamansible-core 2.14.18
AlmaLinux 10, AppStreamansible-core 2.16.16
PyPI, pipx ileansible 14.5.0, ansible-core 2.21.5 üzerinde

ansible-core 2.14 ve 2.16 artık upstream'de desteklenmiyor; bu sürümlerin düzeltmeleri dağıtımın bakımına bağlı. Güncel sürüm serileri ve Python gereksinimleri için sürüm ve bakım sayfasına bakın.

Envanter (inventory): hangi sunucular, hangi gruplar?​

Projeye bir ansible.cfg koyun, her komutta -i yazmak zorunda kalmayın. Ansible yalnızca bulduğu ilk yapılandırma dosyasını kullanır; arama sırası şöyledir: ANSIBLE_CONFIG ortam değişkenindeki yol, ./ansible.cfg, ~/.ansible.cfg, /etc/ansible/ansible.cfg.

INI:
# ansible.cfg
[defaults]
inventory = inventory/hosts.ini

# inventory/hosts.ini  (web1: AlmaLinux 9, web2: Ubuntu 24.04)
[web]
web1 ansible_host=203.0.113.10
web2 ansible_host=203.0.113.11

[db]
db1 ansible_host=203.0.113.20 ansible_port=2222

[prod:children]
web
db

Aynı envanterin YAML biçimi; listeleri ve iç içe değerleri daha rahat taşır:

YAML:
all:
  children:
    web:
      hosts:
        web1:
          ansible_host: 203.0.113.10
        web2:
          ansible_host: 203.0.113.11
    db:
      hosts:
        db1:
          ansible_host: 203.0.113.20
          ansible_port: 2222
    prod:
      children:
        web:
        db:

Değişkenleri sunucu satırlarına yığmayın. Ansible, envanterin yanındaki group_vars/ ve host_vars/ klasörlerini kendiliğinden yükler: all.yml her sunucuya, web.yml web grubuna, host_vars/web1.yml tek bir sunucuya uygulanır.

YAML:
# inventory/group_vars/all.yml
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
admin_user: deploy
admin_pubkey_file: files/deploy_ed25519.pub

En sık kullanacağınız bağlantı değişkenleri ansible_host, ansible_port, ansible_user, ansible_ssh_private_key_file, ansible_become ve ansible_python_interpreter. Bir şey çalıştırmadan önce Ansible'ın ne gördüğünü kontrol edin:

Bash:
ansible-inventory --graph              # grup ağacı
ansible-inventory --graph db --vars    # tek grup, değişkenleriyle birlikte
ansible-inventory --host web1          # bir sunucunun birleştirilmiş değişkenleri

Bulut ortamlarında amazon.aws.aws_ec2 ya da hetzner.hcloud.hcloud gibi envanter eklentileri (inventory plugin) sunucu listesini sağlayıcının API'sinden okur. Her biri, adını eklentinin tanıdığı küçük bir YAML yapılandırması ister; örneğin hcloud eklentisi adı hcloud.yml ile biten bir dosya bekler.

Ad-hoc komutlar: hızlı kontrol için, yapılandırma için değil​

Bash:
ansible all -m ansible.builtin.ping                  # SSH girişi + Python kontrolü, ICMP değil
ansible web -a "uptime"                              # varsayılan modül: command
ansible web -b -m ansible.builtin.shell -a "ss -tlnp | grep -E ':(80|443) '"
ansible web -m ansible.builtin.setup -a "filter=ansible_distribution*"
ansible web1 -b -m ansible.builtin.service -a "name=nginx state=restarted"

ping modülü, SSH girişi çalışıyor ve kullanılabilir bir Python bulunuyorsa pong döner; bu yüzden her yeni sunucuda ilk test budur. command modülü programı kabuk (shell) olmadan çalıştırır, dolayısıyla boru (pipe), yönlendirme ve joker karakterler orada çalışmaz; bunlara ihtiyacınız varsa shell kullanın, daha iyisi işe uygun modülü seçin. -b görevi sudo üzerinden çalıştırır, -K sudo parolasını sorar. Ad-hoc komutlar hızlı sorgular ve tek seferlik acil müdahaleler içindir; iki kez yazacağınız her şey playbook'a girmeli, çünkü orada gözden geçirilebilir ve tekrarlanabilir.

AlmaLinux ve Ubuntu için ilk gerçek playbook​

Aşağıdaki playbook SSH anahtarı ve parolasız sudo yetkisi olan bir yönetici kullanıcı oluşturur, sshd'yi sıkılaştırır (hardening), SSH, HTTP ve HTTPS trafiğine izin verir (AlmaLinux'ta firewalld, Ubuntu'da ufw ile) ve nginx kurar. Tek dosya iki aileye de ansible_facts['os_family'] üzerinden hizmet eder: bu değer AlmaLinux'ta RedHat, Ubuntu'da Debian'dır. ansible.posix ve community.general, ansible paketiyle birlikte gelir; yalnızca ansible-core kullanıyorsanız önce bunları kurun. Bu adımların neden ilk sırada geldiğini canlı Linux sunucular için sunucu sıkılaştırma yazımızda anlattık.

YAML:
# site.yml
---
- name: Web sunucuları için temel yapılandırma
  hosts: web
  become: true

  vars:
    admin_group: "{{ 'wheel' if ansible_facts['os_family'] == 'RedHat' else 'sudo' }}"
    ssh_service: "{{ 'sshd' if ansible_facts['os_family'] == 'RedHat' else 'ssh' }}"

  tasks:
    - name: Yönetici kullanıcıyı oluştur
      ansible.builtin.user:
        name: "{{ admin_user }}"
        groups: "{{ admin_group }}"
        append: true
        shell: /bin/bash
      tags: [users]

    - name: Yöneticinin açık anahtarını ekle
      ansible.posix.authorized_key:
        user: "{{ admin_user }}"
        key: "{{ lookup('ansible.builtin.file', admin_pubkey_file) }}"
        state: present
      tags: [users]

    - name: Yöneticiye parolasız sudo ver
      ansible.builtin.copy:
        dest: "/etc/sudoers.d/{{ admin_user }}"
        content: "{{ admin_user }} ALL=(ALL) NOPASSWD: ALL\n"
        owner: root
        group: root
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s
      tags: [users]

    - name: SSH ayarlarını drop-in dosyasıyla sıkılaştır
      ansible.builtin.template:
        src: templates/sshd-hardening.conf.j2
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        owner: root
        group: root
        mode: "0600"
        validate: /usr/sbin/sshd -t -f %s
      notify: SSH servisini yeniden yükle
      tags: [ssh]

    - name: Apt önbelleğini yenile
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_facts['os_family'] == 'Debian'

    - name: Nginx ve güvenlik duvarı paketini kur
      ansible.builtin.package:
        name:
          - nginx
          - "{{ 'firewalld' if ansible_facts['os_family'] == 'RedHat' else 'ufw' }}"
        state: present

    - name: AlmaLinux güvenlik duvarı (firewalld)
      when: ansible_facts['os_family'] == 'RedHat'
      tags: [firewall]
      block:
        - name: Firewalld servisini başlat
          ansible.builtin.service:
            name: firewalld
            state: started
            enabled: true

        - name: SSH, HTTP ve HTTPS trafiğine izin ver
          ansible.posix.firewalld:
            service: "{{ item }}"
            permanent: true
            immediate: true
            state: enabled
          loop: [ssh, http, https]

    - name: Ubuntu güvenlik duvarı (ufw)
      when: ansible_facts['os_family'] == 'Debian'
      tags: [firewall]
      block:
        - name: UFW açılmadan önce SSH, HTTP ve HTTPS izni ver
          community.general.ufw:
            rule: allow
            port: "{{ item }}"
            proto: tcp
          loop: ["22", "80", "443"]

        - name: UFW'yi etkinleştir
          community.general.ufw:
            state: enabled

    - name: Nginx servisini başlat
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: SSH servisini yeniden yükle
      ansible.builtin.service:
        name: "{{ ssh_service }}"
        state: reloaded

Kod:
# templates/sshd-hardening.conf.j2
# Ansible tarafından yönetilir. Elle yapılan değişikliklerin üzerine yazılır.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no

  • FQCN adları (ansible.builtin.user gibi tam nitelikli koleksiyon adları) hangi modülün çalışacağını kesin olarak belirtir; ansible-lint bunları denetler.
  • become: true görevleri sudo ile çalıştırır; sunucuya yine ansible_user olarak bağlanırsınız.
  • Fact'ler: ansible_facts['os_family'] biçimini kullanın. Ayrı değişken olarak eklenen ansible_os_family biçimi ansible-core 2.20'den beri kullanımdan kaldırma (deprecation) uyarısı veriyor ve 2.24'te varsayılan olarak kapanacak.
  • when, block, loop: when, koşul yanlışsa görevi ya da bloğu atlar; loop bir görevi her öğe için tekrarlar.
  • validate, yeni dosya eskisinin yerine geçmeden önce onu visudo -cf veya sshd -t -f ile denetler; böylece bir yazım hatası sizi sunucunun dışında bırakmaz, yalnızca görevi başarısız yapar.
  • Handler'lar yalnızca kendilerini bildiren görev changed döndüğünde, play'in o anki bölümünün sonunda (önce pre_tasks, sonra roles ve tasks, sonra post_tasks) ve kaç görev bildirirse bildirsin tek bir kez çalışır.
  • tags playbook'un yalnızca bir kısmını çalıştırmanızı sağlar: --tags ssh ya da --skip-tags firewall.

Neden drop-in dosyası? AlmaLinux 9 ve 10 ile Ubuntu, sshd_config dosyasının başına yakın bir yerde /etc/ssh/sshd_config.d/*.conf dosyalarını Include ile içeri alır. sshd çoğu anahtar kelime için okuduğu ilk değeri kullanır ve içeri alınan dosyalar sözlük sırasına (lexical order) göre okunur. Bu yüzden 00-hardening.conf, AlmaLinux kurulum programının parolayla root girişine izin verildiğinde yazdığı 01-permitrootlogin.conf dosyasından ve bulut imajlarının PasswordAuthentication yes ile yazabildiği 50-cloud-init.conf dosyasından önce okunur ve onlara karşı kazanır. Sonuçta geçerli olan (effective) değerleri sudo sshd -T ile görebilirsiniz. Portu bilerek 22'de bıraktık: Ubuntu 22.10 ve sonrasında sshd systemd soket etkinleştirmesiyle (socket activation) başlatılır, bu yüzden port değişikliği ayrıca systemctl daemon-reload ve ssh.socket'in yeniden başlatılmasını gerektirir; AlmaLinux'ta ise önce SELinux'un yeni porta izin vermesi gerekir (semanage port -a -t ssh_port_t -p tcp PORT).

İlk çalıştırma, sağlayıcının verdiği hesapla yapılan bir ön kurulumdur (bootstrap): root ya da imajın varsayılan kullanıcısı, örneğin Ubuntu bulut imajlarında ubuntu. Bu hesabı -e ile verin; envanter değişkenleri komut satırı seçeneklerinden önceliklidir ve -u, ansible_user=deploy karşısında kaybeder.

Bash:
ansible-playbook site.yml --syntax-check
ansible-playbook site.yml -e ansible_user=root --limit web1   # tek sunucuda ön kurulum
ssh [email protected]                     # ilk oturumu kapatmadan, ikinci bir terminalden
ansible-playbook site.yml --check --diff    # sonraki her değişiklikte önce kuru çalıştırma
ansible-playbook site.yml                   # sonra uygulayın; ansible_user=deploy group_vars'tan gelir

Yeni bir sunucunun ilk çalıştırmasında --check kullanmayın: kullanıcı görevi deploy kullanıcısını yalnızca kâğıt üzerinde oluşturur, bu yüzden anahtar görevi "Either user must exist or you must provide full path to key file in check mode" hatasıyla durur; henüz kurulmamış bir şeye dayanan başka görevler de aynı şekilde takılabilir. Sunucu bir kez kurulduktan sonra --check --diff her değişikliğin doğru ilk adımıdır. Yeni kullanıcıyla (deploy) giriş yapabildiğinizi görene kadar ilk oturumu açık tutun. ufw kuralları ufw etkinleştirilmeden önce eklenir; firewalld'nin varsayılan public bölgesi (zone) SSH trafiğine zaten izin verir. SSH başka bir portta dinliyorsa o portu, güvenlik duvarı devreye girmeden önce açın.

Playbook'ları güvenli çalıştırmak​

  • --syntax-check, --list-tasks, --list-hosts yapı hatalarını yakalar ve neyin nerede çalışacağını gösterir.
  • --check --diff farkları gösteren bir kuru çalıştırmadır (dry run). Check mode'u desteklemeyen modüller bu modda atlanır, önceki bir değişikliğe bağlı görevler hata verebilir; sonucu kesin kanıt değil, güçlü bir ipucu sayın.
  • --limit web1 değişikliği önce bir kanarya sunucuya gönderir.
  • serial play'i gruplar hâlinde yürütür (1, "30%" ya da [1, 5, "25%"]). Varsayılan olarak play yalnızca bir gruptaki sunucuların tamamı başarısız olduğunda durur; dağıtımı daha erken durdurmak için max_fail_percentage ayarlayın (0, ilk hatada durdurur).
  • changed_when ve failed_when command görevlerini dürüst tutar; örnekler aşağıda.
  • Bir görevi silmek hiçbir şeyi geri almaz: görevi kaldırma biçimine çevirip bir kez daha çalıştırın (paket, kullanıcı ve dosyalar için state: absent, firewalld servisi için state: disabled, ufw kuralı için delete: true). ufw modülünün belgeleri, allow görevini yalnızca silmenin portu açık bırakabileceği konusunda uyarır.

YAML:
- name: Nginx yapılandırmasını doğrula
  ansible.builtin.command: nginx -t
  changed_when: false               # kontrol hiçbir şeyi değiştirmez

- name: Eski cron kaydını ara
  ansible.builtin.command: grep -q legacy-backup /etc/crontab
  register: legacy
  changed_when: false
  failed_when: legacy.rc > 1        # 0 = bulundu, 1 = bulunamadı, 2 = hata

- name: Django migration'larını uygula
  ansible.builtin.command:
    cmd: .venv/bin/python manage.py migrate --noinput
    chdir: /srv/app
  register: migrate
  changed_when: "'No migrations to apply' not in migrate.stdout"

Roller ve koleksiyonlar​

Playbook büyüdüğünde ansible-galaxy role init roles/baseline komutu iskeleti oluşturur: defaults/, files/, handlers/, meta/, tasks/, templates/, tests/, vars/ ve README.md. Görevleri, handler'ı, şablonu ve anahtarı bu yapıya taşıyın; site.yml de hosts, become: true ve roles altındaki baseline rolünden oluşan kısa bir play'e iner. Ayarlanabilir değerleri ve platform sabitlerini ayrı tutun:

YAML:
# roles/baseline/defaults/main.yml : kullanıcının değiştirebileceği ayarlar
baseline_admin_user: deploy
baseline_admin_pubkey_file: deploy_ed25519.pub
baseline_passwordless_sudo: true

# roles/baseline/vars/RedHat.yml : platform sabitleri
baseline_admin_group: wheel
baseline_ssh_service: sshd
baseline_firewall_package: firewalld

# roles/baseline/vars/Debian.yml
baseline_admin_group: sudo
baseline_ssh_service: ssh
baseline_firewall_package: ufw

# roles/baseline/tasks/main.yml içindeki ilk görev
- name: İşletim sistemi ailesinin değişkenlerini yükle
  ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"

Gerçek hatalara yol açan öncelik (precedence) kuralları şunlar (tam liste değişkenler rehberinde):
  • Rol varsayılanları (defaults) sıralamanın en altına yakındır; envanterdeki group_vars, host_vars ve play değişkenleri onları ezer. Ayarlanabilir değerlerin yeri burasıdır.
  • Rolün vars dosyaları ve include_vars, envanter değişkenlerinden önceliklidir; buraya yazılan bir değer group_vars ile değiştirilemez. "Ansible group_vars dosyamı görmüyor" şikâyetinin klasik nedeni budur. Burada yalnızca sabitleri tutun.
  • Ekstra değişkenler (-e) her zaman kazanır.
  • Rol değişkenlerine rol adını önek olarak verin (baseline_...); ansible-lint'in var-naming kuralı bunu denetler.

Dış koleksiyonlar ve roller requirements.yml dosyasına yazılır:

YAML:
# requirements.yml
collections:
  - name: ansible.posix
    version: ">=2.2.0,<3.0.0"
  - name: community.general
    version: ">=13.0.0,<14.0.0"
roles:
  - name: company_nginx
    src: git+https://git.example.com/infra/ansible-role-nginx.git
    version: v1.4.0

ansible-galaxy install -r requirements.yml iki bölümü de kurar; ansible-galaxy collection list hangi sürümlerin etkin olduğunu gösterir. Dosyayı depoya işleyin (commit) ki dizüstü bilgisayarlar ve CI aynı sürüm aralıklarından kurulum yapsın; her çalıştırmada birebir aynı kod gerekiyorsa sürümleri tam olarak sabitleyin.

Ansible Vault ile gizli bilgiler​

Vault, tek tek değerleri ya da dosyaların tamamını AES256 ile şifreler; böylece bu değerler git deposunda durabilir. Vault kimliği (vault ID) denen prod gibi bir etiket, Ansible'a hangi parolayı kullanacağını söyler. Parolayı deponun dışında tutun:

Bash:
mkdir -p ~/.ansible && install -m 600 /dev/null ~/.ansible/vault-pass-prod
# parolayı bu dosyaya yazın, ardından tek bir değeri şifreleyin:
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-pass-prod --prompt
#   Variable name (enter for no name): vault_db_password
#   String to encrypt (hidden):

--prompt gizli değeri ekranda göstermeden okur, böylece değer kabuk geçmişine (shell history) düşmez. Ardından şifresiz bir değişkeni şifreli olana yönlendirin; Ansible belgelerinin önerdiği vars/vault düzeni budur: değişken adları grep ile aranabilir kalır, değerler şifreli kalır.

YAML:
# inventory/group_vars/web/vault.yml
vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          37613230326661303866366665386439346435646163316435396238663261323065363366386131
          ...

# inventory/group_vars/web/vars.yml
db_password: "{{ vault_db_password }}"

Parola dosyasını bir kez ansible.cfg içinde belirtin, diğer komutlar onu kendiliğinden bulur:

Bash:
# ansible.cfg, [defaults] bölümü:
#   vault_identity_list = prod@~/.ansible/vault-pass-prod
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-vault view secrets.yml
ansible-vault rekey --new-vault-id prod@prompt secrets.yml   # parolayı değiştir
ansible-playbook site.yml                                     # şifre otomatik çözülür

  • encrypt_string ile şifrelenmiş tekil değerler rekey ile yeni parolaya taşınamaz; şifrelenmiş dosyalar taşınabilir.
  • Gizli bilgiyle çalışan görevlerde no_log: true, gizli bilgi içeren şablonlarda diff: false kullanın; aksi hâlde --diff gizli değeri ekrana basar.
  • Parola dosyasını asla depoya işlemeyin.
  • Kısa ömürlü ya da birden çok sistemin paylaştığı gizli bilgiler için harici bir gizli bilgi yöneticisi (secret store) daha uygundur; örneğin community.hashi_vault koleksiyonu üzerinden HashiCorp Vault. Bu araçlarla ilgili sorular için bu bölümdeki Vault ve Gizli Anahtarlar (Secrets) alt forumlarını kullanabilirsiniz.

Lint, test ve CI​

  • ansible-lint iyi uygulama kurallarını denetler: FQCN kullanımı, dosya izinleri, changed_when içermeyen command görevleri ve yamllint üzerinden YAML biçimi; bu yüzden ayrıca yamllint çalıştırmak isteğe bağlıdır. Profiller sıkılığı min, basic, moderate, safety, shared ve production sırasıyla artırır; ansible-lint --profile=basic ile başlayın. Bu yazıdaki örnekler ansible-lint 26.10.1 ile production profilinden geçiyor.
  • Molecule bir rolü geçici makinelerde, genellikle konteynerlerde test eder. molecule test komutu, rolü ikinci kez uygulayan ve hâlâ changed dönen bir görev varsa başarısız olan bir idempotence adımı içerir.
  • Linter'ı pipx install ansible-lint ile kurun. ansible-dev-tools paketi ansible-lint, Molecule ve ansible-navigator dâhil araç setinin tamamını içerir; belgeleri kurulum için pip kullandığından paketi bir sanal ortama kurun.

Playbook çalıştıran bir CI işi, CI sisteminin gizli bilgi deposundan (secret store) üç değer ister: ayrı bir deploy kullanıcısının SSH özel anahtarı, sabitlenmiş known_hosts satırları ve vault parolası. Ansible çalıştırılabilir bir parola dosyasını çalıştırıp çıktısını okuduğu için parola, küçük bir betik aracılığıyla ortam değişkeninden gelebilir:

Bash:
#!/bin/sh
# vault-pass.sh: çalıştırma izniyle (chmod +x) depoya işleyin; gizli bilgi içermez
printf '%s\n' "$ANSIBLE_VAULT_PASSWORD"

Bash:
# CI işinin adımları (GitHub Actions, GitLab CI, Jenkins); gizli bilgiler ortam değişkeni olarak gelir
install -d -m 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts   # bir kez ssh-keyscan ile toplandı, parmak izleri doğrulandı
export ANSIBLE_VAULT_IDENTITY_LIST=prod@./vault-pass.sh   # ansible.cfg içindeki dizüstü yolunu geçersiz kılar
pipx install --include-deps ansible && pipx install ansible-lint
ansible-galaxy install -r requirements.yml
ansible-lint
ansible-playbook site.yml --check --diff   # bu deponun dallarından gelen pull request'ler
ansible-playbook site.yml                  # main dalı, onaydan sonra

ANSIBLE_VAULT_IDENTITY_LIST, ansible.cfg'den önceliklidir ve ansible-lint de onu okur; böylece aynı depo hem dizüstü bilgisayarda hem CI'da çalışır. Betiğin çalıştırma izni kaybolursa Ansible betik metnini parola sanar ve şifre çözme başarısız olur. Pipeline yeşile dönsün diye host_key_checking = False ayarlamayın: sabitlenmiş sunucu anahtarları, pipeline'ın gizli bilgileri yanlış makineye göndermesini engeller. Fork'lardan gelen pull request'leri de gizli bilgilerden uzak tutun: checkout edilen koddaki bir ansible.cfg çalıştırılabilir bir parola dosyasını gösterebilir ve Ansible ya da ansible-lint o dosyadaki kodu çalıştırır. Gizli bilgilerin kapsamını ve üretim onay kapılarını uzun ömürlü CI/CD pipeline'ları yazımızda ele aldık.

Çalıştırmaları hızlandırmak​

Ansible varsayılan olarak aynı anda 5 sunucuyla çalışır ve her play'in başında tüm fact'leri toplar. Ayarlanmış bir ansible.cfg örneği:

INI:
[defaults]
inventory = inventory/hosts.ini
forks = 20
gathering = smart
fact_caching = ansible.builtin.jsonfile
fact_caching_connection = ~/.cache/ansible/facts
fact_caching_timeout = 7200
callbacks_enabled = ansible.posix.profile_tasks

[ssh_connection]
pipelining = True
ssh_args = -C -o ControlMaster=auto -o ControlPersist=10m

  • forks: paralel işlenen sunucu sayısı. Her fork ayrı bir süreçtir; tavanı kontrol düğümünün işlemcisi ve belleği belirler.
  • pipelining, modülleri önce dosya kopyalamadan açık SSH oturumu üzerinden çalıştırır ve görev başına SSH işlemi sayısını azaltır. sudo'nun requiretty seçeneğiyle çakıştığı için varsayılan olarak kapalıdır; açmadan önce sudo grep -r requiretty /etc/sudoers /etc/sudoers.d ile kontrol edin.
  • ControlPersist her sunucu için tek bir bağlantıyı yeniden kullanır. Varsayılan ssh_args bağlantıyı zaten 60 saniye açık tutar; kendi ssh_args değeriniz varsayılanın tamamının yerine geçer, bu yüzden ControlMaster=auto ayarını içinde bırakın.
  • Fact'ler: gathering = smart ve bir önbellek, aynı bilgilerin tekrar tekrar toplanmasını önler. Fact okumayan play'lerde gather_facts: false kullanın; gather_subset: ["!all"] yalnızca asgari kümeyi toplar ve os_family bu kümeye dâhildir.
  • profile_tasks her görevin ne kadar sürdüğünü yazar; böylece tahmin yürütmek yerine gerçekten yavaş olan görevleri iyileştirirsiniz.
  • Bir bastion sunucusu üzerinden çalışırken her fork bastion'a kendi bağlantısını açar; sshd'nin MaxStartups ayarı (varsayılan 10:30:100), kimliği doğrulanmamış 10 bağlantı beklemedeyken yeni bağlantıların bir kısmını reddetmeye başlar. Bastion'da MaxStartups değerini yükseltin ya da forks değerini ölçülü tutun.

Her ayarın kazancı ağ gecikmesine, sunucu sayısına ve görevlerin kendisine bağlıdır; bir değişiklikten önce ve sonra profile_tasks ile ölçün.

Sık karşılaşılan hatalar ve çözümleri​

Mesaj veya belirtiOlası nedenÇözüm
UNREACHABLE ... Permission denied (publickey)Yanlış kullanıcı veya anahtar ya da anahtar sunucuda yokssh -i anahtar kullanici@sunucu ile elle deneyin; ansible_user ve ansible_ssh_private_key_file değerlerini kontrol edin; -vvvv ile tekrar çalıştırın
Host key verification failedİlk bağlantı ya da sunucu yeniden kurulduParmak izini karşılaştırın, sonra ekleyin (ssh-keyscan) ya da eskisini silin (ssh-keygen -R); sunucu anahtarı denetimini genel olarak kapatmayın
Missing sudo passwordbecome bir parola istiyor ama Ansible'a verilmedi-K ekleyin, otomasyon kullanıcısına NOPASSWD sudo verin ya da ansible_become_password değerini Vault'ta tutun
Host 'web1' is using the discovered Python interpreter at ...Yorumlayıcı keşfi (interpreter discovery) uyarısıgroup_vars içinde ansible_python_interpreter: /usr/bin/python3 tanımlayın ya da interpreter_python = auto_silent kullanın
couldn't resolve module/action 'community.general.ufw'Koleksiyon eksik (yalnızca ansible-core kurulu) ya da yazım hatasıansible-galaxy install -r requirements.yml çalıştırın; ansible-galaxy collection list ile kontrol edin
YAML parsing failed: This may be an issue with missing quotes around a template block.Değer {{ ile başlıyor ve YAML onu sözlük (dictionary) sanıyorDeğerin tamamını tırnak içine alın: msg: "{{ admin_user }}"
Conditional result (True) was derived from value of type 'str' ... Conditionals must have a boolean result.2.19'dan beri when gerçek bir boolean bekliyorTesti açıkça yazın: admin_user | length > 0; evet/hayır metinleri için var | bool
Either user must exist or you must provide full path to key file in check modeKullanıcıyı önceki bir görevin oluşturacağı sunucuda --checkYeni sunucuda beklenen bir durum; ön kurulumu gerçekten yapın, sonra check mode kullanın
Handler hiç çalışmıyorGörev changed değil ok döndü ya da sonraki bir görev hata verdi ve play handler'a gelmeden durduGörev sonucunu kontrol edin; --force-handlers veya force_handlers: true kullanın; meta: flush_handlers handler'ları daha erken çalıştırır
The requested handler 'Reload SSHD' was not found ...notify metni handler adından farklı; ad büyük/küçük harfe duyarlıAdı birebir kopyalayın ya da listen konularını (topic) kullanın
provided hosts list is empty ya da Could not match supplied host patternYanlış envanter yolu ya da herkesin yazabildiği bir dizinde yok sayılan ansible.cfgansible --version kullanılan yapılandırma dosyasını, ansible-config dump --only-changed değiştirilmiş ayarları gösterir
Gathering Facts aşaması yavaşHer play'de tüm fact'ler toplanıyorgather_facts: false, gather_subset ya da fact önbelleği (yukarıya bakın)

Sık sorulan sorular​

Ansible mı, Terraform mu?
Terraform (ya da OpenTofu) bulut kaynaklarını oluşturur ve bir state dosyasıyla takip eder; Ansible işletim sistemlerini ve uygulamaları yapılandırır. İkisi birlikte iyi çalışır. Ansible da bulut API'lerini çağırabilir ama state tutmaz; artık tanımlamadığınız kaynakları kaldırmak sizin işinizdir.

Neden düz shell betikleri değil?
Betik komut çalıştırır; Ansible modülleri bir son durum tanımlar ve önce mevcut durumu kontrol eder. Güvenli tekrar çalıştırma, farkları gösteren kuru çalıştırma, tüm sunucular için tek envanter ve paralel çalışma elde edersiniz; sunucu sayısı birkaçı geçtiğinde bunların değeri hissedilir.

Ansible sunucularda ajan (agent) gerektirir mi?
Hayır. Yönetilen düğümlerde SSH erişimi ve ansible-core 2.21 için Python 3.9 veya üstü yeterlidir. Ağ cihazları ayrı bir durumdur: onların modülleri genellikle cihazda Python gerektirmez.

Ansible Windows sunucuları yönetebilir mi?
Evet, yönetilen düğüm olarak: ansible.windows koleksiyonuyla WinRM (winrm veya psrp eklentisi) ya da SSH üzerinden. SSH, ansible-core 2.18'den beri Windows ile gelen OpenSSH kullanılarak resmî olarak destekleniyor; bu pratikte Windows Server 2022 ve sonrası demek. Kontrol düğümü yine Linux, macOS ya da BSD olmalı.

Web arayüzü var mı?
Ticari seçenek Red Hat Ansible Automation Platform'dur. Onun upstream projelerinden biri olan AWX, web arayüzü ve REST API sunar; ancak GitHub'daki README dosyası son sürümün Temmuz 2024'te çıktığını ve büyük bir yeniden yapılanma sürerken yeni sürümlerin durdurulduğunu belirtiyor. MIT lisanslı Semaphore UI bir alternatif; birçok küçük ekip ise playbook'ları doğrudan CI'dan çalıştırıyor.

Sonuç ve sonraki adımlar​

Ansible, sunucularınıza değişikliklerin ulaştığı tek yol hâline geldiğinde karşılığını verir. Mantıklı bir sıra şöyle: pipx ile kurun, envanteri yazın ve ansible all -m ansible.builtin.ping komutunun her sunucudan yanıt aldığını görün; temel playbook ile tek bir sunucuda --limit kullanarak ön kurulum yapın, sonraki her değişikliğe --check --diff ile başlayın; playbook'u bir role taşıyıp ansible-lint ekleyin; ardından gizli bilgileri Vault'a alın ve playbook'u CI'ın çalıştırmasına bırakın. Sürümler değiştiğinde başvuru kaynağı resmî kurulum rehberidir. Sunucuların işletimini başkasına bırakmayı tercih ederseniz AIOR'un barındırma ve sunucu hizmetleri VPS, dedicated ve yönetilen sunucuların yanında AlmaLinux ve Ubuntu sunucu yönetimini de kapsıyor. Kendi kurulumunuzla ilgili soruları bu konuya yazabilirsiniz.

Ansible is an agentless configuration management tool: you describe the desired state of your servers in YAML, and Ansible connects over SSH and changes only what differs. This tutorial builds a working setup for a small mixed fleet of AlmaLinux and Ubuntu servers, from installation and inventory to a first real playbook, roles, Ansible Vault, CI, speed tuning and troubleshooting. Commands and versions were checked against ansible-core 2.21 and the Ansible 14 package in October 2026, and the example playbooks pass --syntax-check and ansible-lint. Still, try them on a test server before production.

What Ansible is and how a run works​

Ansible runs on one machine, the control node (a laptop, bastion host or CI runner), and pushes changes to the managed nodes without installing a daemon on them. For each task it connects with the system's OpenSSH client, copies a small Python program (a module) to the server, runs it, reads the JSON result and deletes it. Managed nodes need only SSH and Python 3.9 to 3.14 for ansible-core 2.21, which AlmaLinux 9 and 10 and Ubuntu 22.04 and 24.04 already have. AlmaLinux 8 is the exception: its dnf Python bindings (python3-dnf) are packaged only for the system Python 3.6, which ansible-core stopped supporting on managed nodes in 2.17, so installing a newer Python does not get package management working. Manage EL8 hosts from a separate, older ansible-core environment (2.16, already end of life upstream) or plan their move to AlmaLinux 9 or 10.

Most modules are idempotent: they compare the current state with the requested one and act only on a difference, so a second run of a correct playbook reports ok instead of changed. That makes re-runs safe and is the real difference from a folder of shell scripts. The exceptions are command and shell, since Ansible cannot know what an arbitrary command changes.

TermMeaning
Control nodeWhere Ansible runs: laptop, bastion host, CI runner
Managed nodeA server Ansible configures (SSH + Python)
InventoryHosts, groups and their variables
ModuleA unit of work, e.g. ansible.builtin.user
Task / play / playbookOne module call / tasks for a host group / a YAML file of plays
HandlerA task that runs only when notified by a change
RoleReusable bundle of tasks, handlers, templates and defaults
CollectionPackage of modules, plugins and roles, e.g. community.general
FactsData gathered from each host: OS family, IPs, memory

Where it fits: Terraform or OpenTofu create servers, networks and DNS records, Ansible configures what runs inside the servers, and many teams use both. Our Terraform, Pulumi and Ansible comparison covers that choice, so this guide does not repeat it.

Installing Ansible on the control node​

The control node runs Linux, macOS or a BSD; Windows cannot be one. WSL works for learning, but the Windows guide in the Ansible docs says it is not supported and should not be used for production systems. Inside WSL, keep the project in your Linux home directory rather than under /mnt/c, because Ansible ignores an ansible.cfg in a directory that looks world-writable.

ansible-core is the engine with the ansible.builtin modules; the ansible package adds curated collections such as ansible.posix and community.general (Ansible 14 is built on ansible-core 2.21). ansible-core 2.21 needs Python 3.12 to 3.14 on the control node. Its successor, 2.22, is planned for November 2026 and needs Python 3.13 or newer (its release candidate on PyPI already enforces this), so a Python 3.12 control node stays on 2.21.x, maintained upstream until November 2027. pipx keeps Ansible in its own virtual environment:

Bash:
# Ubuntu 24.04 (Python 3.12)
sudo apt update && sudo apt install -y pipx
pipx ensurepath                      # then open a new shell
pipx install --include-deps ansible

# AlmaLinux 9 (system Python is 3.9, so use the 3.12 package)
sudo dnf install -y python3.12 python3.12-pip
python3.12 -m pip install --user pipx
python3.12 -m pipx ensurepath
pipx install --include-deps ansible

# macOS
brew install pipx && pipx ensurepath
pipx install --include-deps ansible

ansible --version                    # first line: ansible [core 2.21.x]
pipx upgrade --include-injected ansible

--include-deps is required because the commands come from the ansible-core dependency; without it pipx reports that no apps are associated with the package. For the minimal engine, install ansible-core plus the collections listed in a requirements.yml (see roles below). Distribution packages update with the OS but lag behind:

Source (checked October 2026)Version
Ubuntu 24.04, aptansible 9.2.0 on ansible-core 2.16.3
Ubuntu 26.04, aptansible 13.1.0 on ansible-core 2.20.1
Ansible PPA (ppa:ansible/ansible), Ubuntu 24.04ansible 14.5.0 on ansible-core 2.21.5
AlmaLinux 9, AppStreamansible-core 2.14.18
AlmaLinux 10, AppStreamansible-core 2.16.16
PyPI through pipxansible 14.5.0 on ansible-core 2.21.5

Upstream no longer supports ansible-core 2.14 and 2.16, so fixes for those depend on the distribution. The release and maintenance page lists current lines and Python requirements.

Inventory: which servers, which groups​

Put an ansible.cfg in the project so you can drop -i from every command. Ansible uses only the first config file it finds, in this order: the path in ANSIBLE_CONFIG, ./ansible.cfg, ~/.ansible.cfg, /etc/ansible/ansible.cfg.

INI:
# ansible.cfg
[defaults]
inventory = inventory/hosts.ini

# inventory/hosts.ini  (web1: AlmaLinux 9, web2: Ubuntu 24.04)
[web]
web1 ansible_host=203.0.113.10
web2 ansible_host=203.0.113.11

[db]
db1 ansible_host=203.0.113.20 ansible_port=2222

[prod:children]
web
db

The same inventory in YAML, which handles lists and nested values better:

YAML:
all:
  children:
    web:
      hosts:
        web1:
          ansible_host: 203.0.113.10
        web2:
          ansible_host: 203.0.113.11
    db:
      hosts:
        db1:
          ansible_host: 203.0.113.20
          ansible_port: 2222
    prod:
      children:
        web:
        db:

Keep variables out of the host lines. Ansible loads group_vars/ and host_vars/ next to the inventory automatically: all.yml applies to every host, web.yml to the web group, host_vars/web1.yml to one host.

YAML:
# inventory/group_vars/all.yml
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
admin_user: deploy
admin_pubkey_file: files/deploy_ed25519.pub

The connection variables you will use most are ansible_host, ansible_port, ansible_user, ansible_ssh_private_key_file, ansible_become and ansible_python_interpreter. Check what Ansible sees before running anything:

Bash:
ansible-inventory --graph              # group tree
ansible-inventory --graph db --vars    # one group with its variables
ansible-inventory --host web1          # merged variables of one host

For cloud fleets, inventory plugins such as amazon.aws.aws_ec2 or hetzner.hcloud.hcloud read the host list from the provider's API. Each takes a small YAML config whose file name the plugin recognises; hcloud expects one ending in hcloud.yml.

Ad-hoc commands: quick checks, not configuration​

Bash:
ansible all -m ansible.builtin.ping                  # SSH login + Python check, not ICMP
ansible web -a "uptime"                              # default module: command
ansible web -b -m ansible.builtin.shell -a "ss -tlnp | grep -E ':(80|443) '"
ansible web -m ansible.builtin.setup -a "filter=ansible_distribution*"
ansible web1 -b -m ansible.builtin.service -a "name=nginx state=restarted"

ping answers pong when the SSH login works and a usable Python is found, so it is the first test for every new host. command runs a program without a shell, so pipes, redirects and wildcards do not work there; use shell when you need them, or better, a dedicated module. -b runs the task through sudo and -K asks for the sudo password. Ad-hoc commands suit quick queries and one-off emergencies; anything you would type twice belongs in a playbook, where it can be reviewed and repeated.

A first real playbook for AlmaLinux and Ubuntu​

This playbook creates an admin user with an SSH key and passwordless sudo, hardens sshd, opens SSH, HTTP and HTTPS (firewalld on AlmaLinux, ufw on Ubuntu) and installs nginx. One file serves both families through ansible_facts['os_family'], which is RedHat on AlmaLinux and Debian on Ubuntu. ansible.posix and community.general ship with the ansible package; with ansible-core alone, install them first. Our server hardening guide for production Linux explains why these steps come first.

YAML:
# site.yml
---
- name: Baseline for web servers
  hosts: web
  become: true

  vars:
    admin_group: "{{ 'wheel' if ansible_facts['os_family'] == 'RedHat' else 'sudo' }}"
    ssh_service: "{{ 'sshd' if ansible_facts['os_family'] == 'RedHat' else 'ssh' }}"

  tasks:
    - name: Create the admin user
      ansible.builtin.user:
        name: "{{ admin_user }}"
        groups: "{{ admin_group }}"
        append: true
        shell: /bin/bash
      tags: [users]

    - name: Install the admin public key
      ansible.posix.authorized_key:
        user: "{{ admin_user }}"
        key: "{{ lookup('ansible.builtin.file', admin_pubkey_file) }}"
        state: present
      tags: [users]

    - name: Passwordless sudo for the admin user
      ansible.builtin.copy:
        dest: "/etc/sudoers.d/{{ admin_user }}"
        content: "{{ admin_user }} ALL=(ALL) NOPASSWD: ALL\n"
        owner: root
        group: root
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s
      tags: [users]

    - name: Harden sshd with a drop-in file
      ansible.builtin.template:
        src: templates/sshd-hardening.conf.j2
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        owner: root
        group: root
        mode: "0600"
        validate: /usr/sbin/sshd -t -f %s
      notify: Reload sshd
      tags: [ssh]

    - name: Refresh the apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_facts['os_family'] == 'Debian'

    - name: Install nginx and the firewall
      ansible.builtin.package:
        name:
          - nginx
          - "{{ 'firewalld' if ansible_facts['os_family'] == 'RedHat' else 'ufw' }}"
        state: present

    - name: Firewall on AlmaLinux (firewalld)
      when: ansible_facts['os_family'] == 'RedHat'
      tags: [firewall]
      block:
        - name: Start firewalld
          ansible.builtin.service:
            name: firewalld
            state: started
            enabled: true

        - name: Allow SSH, HTTP and HTTPS
          ansible.posix.firewalld:
            service: "{{ item }}"
            permanent: true
            immediate: true
            state: enabled
          loop: [ssh, http, https]

    - name: Firewall on Ubuntu (ufw)
      when: ansible_facts['os_family'] == 'Debian'
      tags: [firewall]
      block:
        - name: Allow SSH, HTTP and HTTPS before enabling ufw
          community.general.ufw:
            rule: allow
            port: "{{ item }}"
            proto: tcp
          loop: ["22", "80", "443"]

        - name: Enable ufw
          community.general.ufw:
            state: enabled

    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload sshd
      ansible.builtin.service:
        name: "{{ ssh_service }}"
        state: reloaded

Kod:
# templates/sshd-hardening.conf.j2
# Managed by Ansible. Local edits will be overwritten.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no

  • FQCN names such as ansible.builtin.user say exactly which module runs; ansible-lint checks for them.
  • become: true runs the tasks through sudo; you still log in as ansible_user.
  • Facts: read ansible_facts['os_family']. The injected ansible_os_family form has raised deprecation warnings since ansible-core 2.20 and is due to be off by default in 2.24.
  • when, block, loop: when skips a task or block where the condition is false; loop repeats a task per item.
  • validate runs visudo -cf or sshd -t -f on the new file before it replaces the old one, so a typo fails the task instead of locking you out.
  • Handlers run only when a notifying task reports changed, at the end of the current section of the play (pre_tasks, then roles and tasks, then post_tasks), and only once however many tasks notified them.
  • tags run part of a playbook: --tags ssh or --skip-tags firewall.

Why a drop-in file? AlmaLinux 9 and 10 and Ubuntu include /etc/ssh/sshd_config.d/*.conf near the top of sshd_config, sshd keeps the first value it reads for most keywords, and included files load in lexical order. 00-hardening.conf therefore sorts ahead of the 01-permitrootlogin.conf that the AlmaLinux installer writes when root login with a password is allowed, and of the 50-cloud-init.conf that cloud images may write with PasswordAuthentication yes. sudo sshd -T shows the values that actually apply. The port stays at 22 on purpose: on Ubuntu 22.10 and later sshd is started through systemd socket activation, so a port change also needs systemctl daemon-reload and a restart of ssh.socket, and on AlmaLinux SELinux must allow the new port first (semanage port -a -t ssh_port_t -p tcp PORT).

The first run is a bootstrap with the account your provider gives you: root, or the image's default user such as ubuntu on Ubuntu cloud images. Pass it with -e, because inventory variables outrank command-line options and -u would lose to ansible_user=deploy.

Bash:
ansible-playbook site.yml --syntax-check
ansible-playbook site.yml -e ansible_user=root --limit web1   # bootstrap one host
ssh [email protected]                     # from a second terminal, before closing the first
ansible-playbook site.yml --check --diff    # every later change: dry run first
ansible-playbook site.yml                   # then apply; ansible_user=deploy comes from group_vars

Skip --check on the very first run of a fresh host: the user task only pretends to create deploy, so the key task stops with "Either user must exist or you must provide full path to key file in check mode", and other tasks that depend on something not yet installed can fail the same way. Once a host is bootstrapped, --check --diff is the right first step for every change. Keep the original session open until a login as deploy works. The ufw rules go in before ufw is enabled, and firewalld's default public zone already allows SSH; if SSH listens on another port, open it before either firewall starts.

Running playbooks safely​

  • --syntax-check, --list-tasks, --list-hosts catch structural errors and show what would run where.
  • --check --diff is a dry run with diffs. Modules without check mode support are skipped, and tasks that depend on an earlier change can fail, so treat it as a strong hint, not proof.
  • --limit web1 sends a change to a canary first.
  • serial rolls a play through batches (1, "30%" or [1, 5, "25%"]). By default the play stops only when every host in a batch fails; set max_fail_percentage (0 stops at the first failure) to halt the rollout sooner.
  • changed_when and failed_when keep command tasks honest, as below.
  • Deleting a task undoes nothing: switch it to the removing form and run it once more (state: absent for packages, users and files, state: disabled for a firewalld service, delete: true for a ufw rule). The ufw module docs warn that simply removing an allow task can leave the port open.

YAML:
- name: Validate the nginx configuration
  ansible.builtin.command: nginx -t
  changed_when: false               # a check never changes anything

- name: Look for a legacy cron entry
  ansible.builtin.command: grep -q legacy-backup /etc/crontab
  register: legacy
  changed_when: false
  failed_when: legacy.rc > 1        # 0 = found, 1 = not found, 2 = error

- name: Apply Django migrations
  ansible.builtin.command:
    cmd: .venv/bin/python manage.py migrate --noinput
    chdir: /srv/app
  register: migrate
  changed_when: "'No migrations to apply' not in migrate.stdout"

Roles and collections​

When the playbook grows, ansible-galaxy role init roles/baseline creates the skeleton: defaults/, files/, handlers/, meta/, tasks/, templates/, tests/, vars/ and README.md. Move the tasks, handler, template and key into it, and site.yml shrinks to a short play: hosts, become: true and the baseline role under roles. Keep tunables and platform constants apart:

YAML:
# roles/baseline/defaults/main.yml : settings users may override
baseline_admin_user: deploy
baseline_admin_pubkey_file: deploy_ed25519.pub
baseline_passwordless_sudo: true

# roles/baseline/vars/RedHat.yml : platform constants
baseline_admin_group: wheel
baseline_ssh_service: sshd
baseline_firewall_package: firewalld

# roles/baseline/vars/Debian.yml
baseline_admin_group: sudo
baseline_ssh_service: ssh
baseline_firewall_package: ufw

# first task in roles/baseline/tasks/main.yml
- name: Load OS family variables
  ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"

The precedence rules that cause real bugs (full list in the variables guide):
  • Role defaults sit near the bottom; inventory group_vars, host_vars and play vars override them. Tunables belong here.
  • Role vars and include_vars outrank inventory variables, so a value there cannot be changed from group_vars: the classic "Ansible ignores my group_vars" bug. Keep only constants there.
  • Extra vars (-e) always win.
  • Prefix role variables with the role name (baseline_...); ansible-lint's var-naming rule checks this.

External collections and roles go into requirements.yml:

YAML:
# requirements.yml
collections:
  - name: ansible.posix
    version: ">=2.2.0,<3.0.0"
  - name: community.general
    version: ">=13.0.0,<14.0.0"
roles:
  - name: company_nginx
    src: git+https://git.example.com/infra/ansible-role-nginx.git
    version: v1.4.0

ansible-galaxy install -r requirements.yml installs both sections, and ansible-galaxy collection list shows which versions are active. Commit the file so laptops and CI install from the same version ranges; pin exact versions when every run must use identical code.

Secrets with Ansible Vault​

Vault encrypts values or whole files with AES256 so they can live in git; a vault ID label such as prod tells Ansible which password to use. Keep the password outside the repository:

Bash:
mkdir -p ~/.ansible && install -m 600 /dev/null ~/.ansible/vault-pass-prod
# put the password into that file, then encrypt one value:
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-pass-prod --prompt
#   Variable name (enter for no name): vault_db_password
#   String to encrypt (hidden):

--prompt reads the secret without echo, so it stays out of shell history. Then point a plain variable at the encrypted one, the vars/vault pattern the Ansible docs recommend: variable names stay searchable with grep, values stay encrypted.

YAML:
# inventory/group_vars/web/vault.yml
vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          37613230326661303866366665386439346435646163316435396238663261323065363366386131
          ...

# inventory/group_vars/web/vars.yml
db_password: "{{ vault_db_password }}"

Name the password file once in ansible.cfg and the other commands find it on their own:

Bash:
# ansible.cfg, [defaults] section:
#   vault_identity_list = prod@~/.ansible/vault-pass-prod
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-vault view secrets.yml
ansible-vault rekey --new-vault-id prod@prompt secrets.yml   # change the password
ansible-playbook site.yml                                     # decrypts automatically

  • Encrypted strings cannot be rekeyed; encrypted files can.
  • Use no_log: true on tasks that handle secrets and diff: false on templates containing them, or --diff will print the secret.
  • Never commit the password file.
  • Short-lived or shared secrets fit an external store better, for example HashiCorp Vault through the community.hashi_vault collection; the Vault and Secrets sub-forums in this section are the place for questions about those tools.

Linting, testing and CI​

  • ansible-lint checks best-practice rules: FQCN, file permissions, command tasks without changed_when, and YAML style through yamllint, so a separate yamllint run is optional. Profiles raise strictness from min through basic, moderate, safety and shared to production; start with ansible-lint --profile=basic. The examples here pass the production profile with ansible-lint 26.10.1.
  • Molecule tests a role on throwaway instances, usually containers. molecule test includes an idempotence step that applies the role a second time and fails if anything still reports changed.
  • Install the linter with pipx install ansible-lint. The ansible-dev-tools package bundles the whole toolset, including ansible-lint, Molecule and ansible-navigator; its documentation installs it with pip, so put it in a virtual environment.

A CI job that runs playbooks needs three values from the CI secret store: the SSH private key of a dedicated deploy user, pinned known_hosts lines and the vault password. The password can arrive as an environment variable through a small script, because Ansible runs an executable password file and reads its output:

Bash:
#!/bin/sh
# vault-pass.sh: commit it with the executable bit (chmod +x); it contains no secret
printf '%s\n' "$ANSIBLE_VAULT_PASSWORD"

Bash:
# CI job steps (GitHub Actions, GitLab CI, Jenkins); secrets arrive as environment variables
install -d -m 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts   # collected once with ssh-keyscan, fingerprints checked
export ANSIBLE_VAULT_IDENTITY_LIST=prod@./vault-pass.sh   # overrides the laptop path in ansible.cfg
pipx install --include-deps ansible && pipx install ansible-lint
ansible-galaxy install -r requirements.yml
ansible-lint
ansible-playbook site.yml --check --diff   # pull requests from branches of this repository
ansible-playbook site.yml                  # main branch, after approval

ANSIBLE_VAULT_IDENTITY_LIST takes precedence over ansible.cfg and ansible-lint reads it too, so one repository works on laptops and in CI. If the script loses its executable bit, Ansible reads the script text as the password and decryption fails. Do not set host_key_checking = False to get a green build: pinned host keys stop a pipeline from sending secrets to the wrong machine. Keep secrets away from fork pull requests as well, because an ansible.cfg in the checkout can point to an executable password file, and Ansible or ansible-lint would run code from it. Our article on CI/CD pipelines that survive covers secret scoping and production approval gates.

Making runs faster​

By default Ansible works on 5 hosts at a time and gathers all facts at the start of every play. A tuned ansible.cfg:

INI:
[defaults]
inventory = inventory/hosts.ini
forks = 20
gathering = smart
fact_caching = ansible.builtin.jsonfile
fact_caching_connection = ~/.cache/ansible/facts
fact_caching_timeout = 7200
callbacks_enabled = ansible.posix.profile_tasks

[ssh_connection]
pipelining = True
ssh_args = -C -o ControlMaster=auto -o ControlPersist=10m

  • forks: hosts handled in parallel. Each fork is a separate process, so the control node's CPU and memory set the ceiling.
  • pipelining runs modules through the open SSH session without copying files first, which cuts SSH operations per task. It is off by default because it conflicts with sudo's requiretty option; check with sudo grep -r requiretty /etc/sudoers /etc/sudoers.d before enabling it.
  • ControlPersist reuses one connection per host. The default ssh_args already keep it for 60 seconds; your own ssh_args replaces the whole default, so keep ControlMaster=auto in it.
  • Facts: gathering = smart plus a cache skips repeated collection. Set gather_facts: false in plays that never read facts; gather_subset: ["!all"] collects only the minimal set, which still includes os_family.
  • profile_tasks prints how long each task took, so you tune the slow ones instead of guessing.
  • Through a bastion host, every fork opens its own connection to it, and sshd's MaxStartups (default 10:30:100) starts refusing some unauthenticated connections once 10 are pending; raise MaxStartups on the bastion or keep forks moderate.

The gain from each setting depends on latency, host count and the tasks themselves, so measure with profile_tasks before and after a change.

Troubleshooting common errors​

Message or symptomLikely causeFix
UNREACHABLE ... Permission denied (publickey)Wrong user or key, or the key is not on the serverTry ssh -i key user@host by hand; check ansible_user and ansible_ssh_private_key_file; rerun with -vvvv
Host key verification failedFirst contact, or the server was rebuiltCompare the fingerprint, then add it (ssh-keyscan) or remove the old one (ssh-keygen -R); do not disable host key checking globally
Missing sudo passwordbecome needs a password Ansible was not givenAdd -K, give the automation user NOPASSWD sudo, or keep ansible_become_password in Vault
Host 'web1' is using the discovered Python interpreter at ...Interpreter discovery warningSet ansible_python_interpreter: /usr/bin/python3 in group_vars, or interpreter_python = auto_silent
couldn't resolve module/action 'community.general.ufw'Collection missing (ansible-core only) or a typoansible-galaxy install -r requirements.yml; check ansible-galaxy collection list
YAML parsing failed: This may be an issue with missing quotes around a template block.A value starts with {{ and YAML reads it as a dictionaryQuote the whole value: msg: "{{ admin_user }}"
Conditional result (True) was derived from value of type 'str' ... Conditionals must have a boolean result.Since 2.19, when needs a real booleanWrite the test out: admin_user | length > 0, or var | bool for yes/no strings
Either user must exist or you must provide full path to key file in check mode--check on a host where an earlier task would create the userExpected on a fresh host; bootstrap it for real, then use check mode
A handler never runsThe task reported ok, not changed, or a later task failed and the play stopped firstCheck the task result; use --force-handlers or force_handlers: true; meta: flush_handlers runs them earlier
The requested handler 'Reload SSHD' was not found ...notify text differs from the handler name, which is case-sensitiveCopy the exact name, or use listen topics
provided hosts list is empty, or Could not match supplied host patternWrong inventory path, or ansible.cfg ignored in a world-writable directoryansible --version shows the config file in use; ansible-config dump --only-changed shows what it set
Gathering Facts is slowFull fact collection in every playgather_facts: false, gather_subset or fact caching (see above)

Frequently asked questions​

Ansible or Terraform?
Terraform (or OpenTofu) creates and tracks cloud resources through a state file; Ansible configures operating systems and applications. They combine well, and although Ansible can call cloud APIs too, it keeps no state, so removing what you no longer declare is your job.

Why not plain shell scripts?
A script runs commands; Ansible modules declare an end state and check it first. You get safe re-runs, dry runs with diffs, one inventory for every host and parallel execution, which starts to matter beyond a handful of servers.

Does Ansible need an agent on the servers?
No. Managed nodes need SSH access and, for ansible-core 2.21, Python 3.9 or newer. Network devices are a separate case: their modules usually need no Python on the device.

Can Ansible manage Windows?
Yes, as managed nodes, with the ansible.windows collection over WinRM (the winrm or psrp plugin) or SSH. SSH has been officially supported since ansible-core 2.18 with the OpenSSH that ships with Windows, which in practice means Windows Server 2022 and later; the control node still has to be Linux, macOS or a BSD.

Is there a web interface?
Red Hat Ansible Automation Platform is the commercial option. AWX, one of its upstream projects, offers a web UI and REST API, but its GitHub README says new releases have been paused since July 2024 during a large refactoring; Semaphore UI is an MIT-licensed alternative, and many small teams simply run playbooks from CI.

Conclusion and next steps​

Ansible pays off once it is the only way changes reach your servers. A sensible order: install it with pipx, write the inventory and get ansible all -m ansible.builtin.ping to answer from every host; bootstrap one server with the baseline playbook and --limit, then make --check --diff the first step of every later change; move the playbook into a role and add ansible-lint; then put secrets into Vault and let CI run the playbook. When versions change, the official installation guide is the reference. If you would rather have someone else run the servers, AIOR's hosting and server services include VPS, dedicated and managed servers as well as AlmaLinux and Ubuntu server management. Questions about your own setup are welcome in this thread.
 
Son düzenleyen:

Forum istatistikleri

Konular
925
Mesajlar
932
Üyeler
26
En son üye
Jessicawatson

Çevrimiçi üyeler

Şu anda çevrimiçi üye yok.

Öne çıkan içerik

AIOR
AIOR TEKNOLOJİ

Tüm ihtiyaçlarınız için Teklif alın

Hosting · Domain · Sunucu · Tasarım · Yazılım · Mühendislik · Sektörel Çözümler

Teklif al

7/24 Destek · Anında yanıt

Geri
Üst