Ansible poza tutorialem: idempotencja, wykrywanie dryftu i playbook, który uratował incydent o 3 w nocy
Demo playbook instaluje nginx i go uruchamia. Działa raz na czystej VM i wszyscy na demie kiwają głowami. To, czego nikt nie demonstruje, to uruchomienie tego samego playbooka sześć miesięcy później na serwerze, gdzie inżynier ręcznie edytował /etc/nginx/nginx.conf żeby tymczasowo naprawić problem produkcyjny i potem zapomniał to udokumentować. Albo po tym, jak pakiet nginx został zaktualizowany przez niezauważony apt cron job. Albo na serwerze, który nigdy nie był właściwie skonwergowany, bo ktoś anulował playbook w połowie.
Produkcyjny Ansible to nie uruchamianie playbooków. To niezawodna konwergencja infrastruktury do znanych stanów, w tym infrastruktury, która odpłynęła od tego, co Ansible ostatnio skonfigurował.
Idempotencja to kontrakt, nie feature
Moduły Ansible są dokumentowane jako idempotentne i większość taka jest. Ale "idempotentny" w Ansible oznacza "uruchomienie tego modułu dwa razy z tymi samymi argumentami daje ten sam wynik", nie oznacza "ten moduł jest bezpieczny do uruchomienia na systemie w nieznanym stanie."
Rozważ popularny wzorzec, który psuje się pod dryftem:
# To wygląda OK. Nie jest OK jeśli serwis był ręcznie wyłączony.
- name: Ensure application service is running
ansible.builtin.service:
name: myapp
state: started
enabled: true
Jeśli inżynier uruchomił systemctl disable myapp --now na serwerze żeby debugować spike CPU, a potem zapomniał, to zadanie raportuje ok (już uruchomiony) lub changed (ponownie włączony), ale nie mówi ci, że ręczna interwencja nastąpiła. Playbook konwerguje stan, ale straciłeś sygnał, że był dryft.
Wzorzec, którego zamiast tego używam:
- name: Check if service has been manually overridden
ansible.builtin.command: systemctl is-enabled myapp
register: svc_enabled
changed_when: false
failed_when: false
- name: Warn on manual override
ansible.builtin.debug:
msg: "WARNING: myapp service is {{ svc_enabled.stdout }} — expected 'enabled'"
when: svc_enabled.stdout != 'enabled'
- name: Converge service state
ansible.builtin.service:
name: myapp
state: started
enabled: true
Ostrzeżenie nie blokuje playbooka. Produkuje widoczny sygnał, że człowiek dokonał zmiany, którą Ansible teraz nadpisuje. W kontekście CI/CD parsuje się ten output i tworzy alert.
Playbook na 3 w nocy
Scenariusz: produkcyjne serwery API zwracają 502. Health checki load balancera nie przechodzą. Inżynier on-call ma 90 sekund zanim klienci to zauważą. Przyczyna: deploy job zakończył się timeoutem w połowie aktualizacji konfiguracji nginx upstream, pozostawiając trzy z ośmiu serwerów ze starą konfiguracją i pięć z nową.
Playbook remediacji piszesz gdy nie jesteś pod presją, żeby gdy jesteś pod presją, uruchomić jedną komendę:
---
- name: Emergency nginx config convergence
hosts: api_servers
serial: 2 # konwerguj dwa naraz, trzymaj 6/8 serwujących ruch
max_fail_percentage: 25 # przerwij jeśli więcej niż 2 serwery zawiodą konwergencję
tasks:
- name: Validate config template renders without errors
ansible.builtin.template:
src: templates/nginx-upstream.conf.j2
dest: /tmp/nginx-upstream-validate.conf
mode: '0600'
changed_when: false
- name: Syntax check the rendered config
ansible.builtin.command: nginx -t -c /tmp/nginx-upstream-validate.conf
changed_when: false
# If nginx -t fails, the play fails here — before touching the live config
- name: Deploy nginx upstream config
ansible.builtin.template:
src: templates/nginx-upstream.conf.j2
dest: /etc/nginx/conf.d/upstream.conf
owner: root
group: root
mode: '0644'
backup: true # keeps upstream.conf.TIMESTAMP on the server
notify: reload nginx
- name: Verify health endpoint responds after reload
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
timeout: 10
retries: 3
delay: 2
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
# reloaded, not restarted — zero downtime config update
serial: 2 to parametr, który ma największe znaczenie. Przy ośmiu serwerach i serial: 2 zawsze masz co najmniej sześć serwerów serwujących ruch podczas konwergencji. Bez tego Ansible konwerguje wszystkie hosty równolegle i dostajesz krótkie okno, gdzie wszystkie osiem jednocześnie przeładowuje nginx, wdrożenie z wiarą i modlitwą w najczystszej postaci.
Vault i sekret, który przypadkowo skomitowałeś
Każdy zespół w końcu commituje sekret do repozytorium Ansible. Podręcznikowa odpowiedź to Ansible Vault. Produkcyjna odpowiedź: Ansible Vault dla sekretów należących do playbooka, zewnętrzne zarządzanie sekretami (HashiCorp Vault, AWS Secrets Manager) dla sekretów współdzielonych między systemami, i no_log: true na każdym zadaniu obsługującym którekolwiek z nich.
- name: Set database credentials in application config
ansible.builtin.template:
src: templates/database.php.j2
dest: /var/www/html/config/database.php
mode: '0640'
vars:
db_password: "{{ lookup('aws_ssm', '/prod/app/db_password', region='eu-west-1') }}"
no_log: true # prevents the rendered template (containing the password) from appearing in logs
no_log: true nie tylko tłumi output zadania, tłumi też output diff. Jeśli uruchamiasz --diff żeby przejrzeć co się zmieniło, nie zobaczysz wyrenderowanego szablonu. To feature, nie bug.
Testowanie playbooków zanim będą miały znaczenie
Dwa narzędzia, których używam do każdej nietrywialnej roli. Molecule do testowania na poziomie roli: odpala kontener lub VM, uruchamia rolę, uruchamia weryfikator (zazwyczaj Testinfra) i sprawdza, że pożądany stan faktycznie został osiągnięty, nie tylko że Ansible zaraportował sukces.
# molecule/default/tests/test_nginx.py
import testinfra
def test_nginx_is_running(host):
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_config_is_valid(host):
result = host.run("nginx -t")
assert result.rc == 0
Tryb --check z --diff przed każdym uruchomieniem produkcyjnym pokazuje co Ansible by zmienił bez faktycznej zmiany. Output diff na zadaniach template jest szczególnie przydatny: widzisz dokładnie które linie w pliku konfiguracyjnym zostałyby zmodyfikowane. Limitowanie do jednego serwera przez --limit api_servers[0] jest niezbędne, bo --check przez cały inventory produkcyjny może zajmować minuty, a na jednym reprezentatywnym serwerze sekundy.
Na co zwracam uwagę w code review Ansible
Zadania command lub shell bez changed_when raportują changed za każdym razem gdy się uruchamiają, nawet jeśli nic się nie zmieniło. To sprawia, że twój diff z --check jest bezużyteczny. ignore_errors: true na czymkolwiek infrastrukturalnym to odpowiednik catch (Exception e) {} bez treści, playbook powinien się zatrzymać, nie kontynuować z potencjalnie uszkodzonym serwerem w puli. Brakujące become: false na zadaniach, które nie potrzebują roota: playbook gdzie każde zadanie działa jako root to playbook, gdzie bug ma blast radius całego serwera.