Automatyzacja
Ansible w praktyce: jak wygląda automatyzacja wdrożeń
Zanim Ansible wszedł na stałe do codziennej pracy, spora część wdrożeń u nas wyglądała tak samo, jak zapewne wygląda w większości firm na pewnym etapie — SSH do serwera, seria komend wklejanych z notatek albo z pamięci, i nadzieja, że nikt nie pominął kroku. Działało, dopóki serwerów było kilka. Przy kilkudziesięciu, z różnym stanem po latach ręcznych zmian, przestało się skalować, a różnice między „identycznymi” serwerami zaczęły powodować realne problemy przy wdrożeniach.
Struktura repozytorium
Repo Ansible trzymam w układzie, który jest w zasadzie standardem społeczności — role jako jednostki wielokrotnego użytku, inventory opisujące, co gdzie należy, i playbooki spinające role w konkretne zadania:
ansible/
├── inventories/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ ├── all.yml
│ │ ├── webservers.yml
│ │ └── databases.yml
│ └── staging/
│ ├── hosts.yml
│ └── group_vars/
├── roles/
│ ├── common/ # bazowa konfiguracja każdego hosta
│ ├── wazuh-agent/
│ ├── haproxy/
│ └── app-deploy/
├── playbooks/
│ ├── site.yml # pełna konfiguracja od zera
│ ├── deploy-app.yml
│ └── patch-security.yml
└── ansible.cfg
Rozdział na production i staging jako osobne inventory, a nie jako zmienną przekazywaną w locie, był świadomą decyzją — chciałem, żeby literówka w nazwie środowiska przy odpalaniu playbooka najwyżej odpaliła się na nieistniejącym inventory (błąd, widoczny od razu), a nie na produkcji przez pomyłkę.
Inventory — nie tylko lista hostów
Grupowanie hostów odzwierciedla realną strukturę infrastruktury, nie tylko topologię sieci. Grupa webservers dostaje inną konfigurację niż databases, a grupy mogą się zagnieżdżać:
all:
children:
production:
children:
webservers:
hosts:
web-prod-01:
web-prod-02:
databases:
hosts:
db-prod-01:
loadbalancers:
hosts:
lb-prod-01:
lb-prod-02:
Zmienne w group_vars per grupa oznaczają, że dodanie nowego web-serwera do produkcji to jedna linijka w inventory, nie kopiowanie konfiguracji z innego hosta i ręczne dostosowywanie.
Przykładowy playbook
Playbook do wdrożenia agenta Wazuh (o którym piszę osobno w tekście o pierwszym wdrożeniu SIEM) na całej grupie serwerów wygląda mniej więcej tak — celowo prosty, bo cała złożoność siedzi w roli, nie w playbooku:
- name: Wdrożenie i konfiguracja agenta Wazuh
hosts: production
become: true
serial: "20%"
roles:
- role: wazuh-agent
wazuh_manager_host: "wazuh.internal.example.com"
wazuh_agent_group: "{{ group_names | first }}"
serial: "20%" to szczegół, który wygląda na drobiazg, dopóki nie zapomnisz go dodać przy zmianie dotykającej setki hostów naraz — playbook bez tego wdroży zmianę na wszystkich serwerach jednocześnie, a jeśli coś jest nie tak z rolą, dowiesz się o tym po fakcie, na całej flocie. Z serial Ansible robi to falami po 20% hostów, sprawdzając wynik przed przejściem do kolejnej fali.
Idempotencja — dlaczego to nie jest tylko modne słowo
Idempotencja oznacza, że odpalenie tego samego playbooka dziesięć razy z rzędu daje ten sam efekt końcowy co odpalenie go raz — Ansible sprawdza aktualny stan i zmienia tylko to, co faktycznie się różni od zadeklarowanego. To jest fundamentalna różnica względem bash-owego skryptu wykonującego komendy sekwencyjnie bez sprawdzania stanu:
- name: Upewnij się, że pakiet fail2ban jest zainstalowany
ansible.builtin.apt:
name: fail2ban
state: present
- name: Wymuś konfigurację jail.local
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
mode: "0644"
notify: restart fail2ban
Moduł apt nie zainstaluje pakietu ponownie, jeśli już jest — sprawdzi stan i zgłosi ok, nie changed. Moduł template nadpisze plik konfiguracyjny tylko wtedy, gdy wygenerowana treść faktycznie się różni od tego, co leży na serwerze — i tylko wtedy wyzwoli handler restartujący usługę. Efekt: mogę odpalić site.yml na całym inventory co noc jako zadanie kontrolne, i jeśli nic się nie zmieniło, playbook przejdzie przez wszystkie hosty bez żadnej realnej zmiany, raportując same ok. To jest w praktyce najlepszy test na to, czy infrastruktura faktycznie odpowiada temu, co jest zadeklarowane w kodzie — a nie temu, co ktoś kiedyś ręcznie poprawił przez SSH i zapomniał zapisać.
shell/command nie są idempotentne z natury — odpalą komendę za każdym razem, niezależnie od stanu. Jeśli używam ich do rzeczy, których nie da się wyrazić modułem natywnym, dodaję jawny warunek creates: albo when: sprawdzający, czy zadanie w ogóle trzeba wykonać. Bez tego playbook „działa”, ale traci całą wartość idempotencji — staje się tym samym skryptem bash, tylko opakowanym w YAML.
Dlaczego to, a nie ręczne SSH
- Spójność — wszystkie serwery w grupie mają tę samą konfigurację, bo powstają z tego samego kodu, nie z pamięci administratora danego dnia.
- Ślad w git — zmiana konfiguracji to commit z opisem i historią, nie polecenie wpisane w terminalu i zapomniane. Kiedy coś się psuje,
git logna roli pokazuje dokładnie, co i kiedy się zmieniło. - Powtarzalność przy katastrofie — odtworzenie serwera po awarii sprzętu to
ansible-playbook site.yml -l nowy-host, nie rekonstruowanie z pamięci, co dokładnie było zainstalowane. - Code review przed produkcją — zmiana w roli przechodzi przez merge request, ktoś inny widzi, co się zmieni, zanim to trafi na serwery.
Integracja z pipeline'em
Playbooki nie są odpalane ręcznie z laptopa na produkcji — podobnie jak z Terraform i Terrakube, o czym pisałem w osobnym tekście o IaC bez apply z laptopa, ten sam nawyk stosuję do Ansible. Pipeline CI odpala playbooki z określonym tagiem na merge do gałęzi głównej, z --check (tryb dry-run) uruchamianym automatycznie na każdy merge request, żeby recenzent widział, co dokładnie się zmieni:
ansible_check:
stage: check
script:
- ansible-playbook -i inventories/production site.yml --check --diff
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
ansible_apply:
stage: apply
script:
- ansible-playbook -i inventories/production site.yml
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Ansible bez pipeline'u to wciąż postęp względem ręcznego SSH. Ansible z pipeline'em i code review to dopiero pełna wartość — nie tylko powtarzalność wykonania, ale i kontrola nad tym, co w ogóle trafia do wykonania.
Ten sam serwer service account (ansible), który wykonuje te playbooki, pojawia się zresztą w regułach Wazuh jako znane, zaufane źródło zmian konfiguracyjnych — bez tej integracji SIEM alarmowałby o każdym uruchomieniu Ansible tak samo, jak o nieautoryzowanej zmianie.