janol.pl_

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ć.

Pułapka: fałszywa idempotencja Niektóre moduły 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 log na 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.

← wszystkie wpisy