Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

Infrastruktura jako kod z Ansible: role dla Ruby, swapa i sshd

Ansible konfiguruje serwery po zwykłym SSH, bez agenta do instalacji. Praktyczny przewodnik po provisioningu serwera aplikacji Ruby idempotentnymi playbookami i reużywalnymi rolami — i dlaczego bezagentowość nas przekonała.

Zarządzanie konfiguracją kiedyś oznaczało Chefa albo Puppeta, a oba żądają tego samego na starcie: zainstaluj agenta na każdym węźle, uruchom centralny serwer i naucz się języka dziedzinowego, zanim położysz choć jeden plik. Dla małego zespołu z garstką serwerów to dużo ceremonii przed jakąkolwiek wartością. Ansible obstawia inaczej — konfiguruje maszyny po zwykłym SSH, bez niczego do zainstalowania na celu poza Pythonem (który już tam jest) — i na naszej infrastrukturze ten zakład w pełni się opłacił.

Oto jak używamy go do provisioningu serwera aplikacji Ruby i jakie idee sprawiają, że Ansible wskakuje na miejsce.

Bezagentowość to cały pomysł

Tym, co nas kupiło, jest to, czego Ansible nie wymaga. Nie ma demona agenta działającego na każdej maszynie, nie ma centralnego mastera do utrzymywania przy życiu i zabezpieczania, nie ma tańca certyfikatów, by zapisać nowy węzeł. Maszyna sterująca — twój laptop albo runner CI — łączy się po SSH, wypycha konfigurację, uruchamia ją Pythonem i rozłącza się. Świeżo zbootowany serwer z kluczem SSH i Pythonem jest natychmiast zarządzalny; nie ma kroku bootstrap, w którym instalujesz narzędzie, zanim możesz go użyć.

Ta właściwość ma uboczne korzyści. „Infrastruktura” to po prostu pliki w repo git — playbooki i role w YAML — więc robi się code review, diffy i rollbacki jak każdego innego kodu. Nie ma stanu po stronie serwera, który by dryfował. A bariera startu jest niemal zerowa, co liczy się bardziej, niż brzmi: narzędzie do zarządzania konfiguracją, które faktycznie wdrożysz, bije potężniejsze, które wciąż zamierzasz skonfigurować.

Inventory i pierwszy playbook

Hosty wymieniasz w pliku inventory, pogrupowane według roli:

# inventory
[web]
web-1 ansible_host=10.0.1.10
web-2 ansible_host=10.0.1.11

[db]
db-1 ansible_host=10.0.2.10

Playbook mapuje grupy hostów na rzeczy, które mają być o nich prawdziwe:

# site.yml
- hosts: web
  become: true            # uruchom z sudo
  roles:
    - common
    - swap
    - ruby
    - nginx
    - deploy

Uruchom ansible-playbook -i inventory site.yml, a Ansible połączy się z każdym hostem w web, równolegle, i zastosuje wymienione role po kolei. Ta sama komenda uruchomiona drugi raz nie powinna zmienić nic — co prowadzi nas do najważniejszej właściwości.

Idempotencja: opisz stan końcowy, nie kroki

Zmiana myślowa, o którą prosi Ansible, jest tą samą, o którą prosi każde dobre narzędzie infrastruktury: przestań pisać skrypty, które robią rzeczy, a zacznij deklarować stan, którego chcesz. Skrypt powłoki uruchamiający adduser deploy zawodzi za drugim razem, bo użytkownik już istnieje. Odpowiednik w Ansible deklaruje pożądany wynik, a moduł ustala, czy potrzebna jest jakakolwiek akcja:

- name: zapewnij istnienie użytkownika deploy
  user:
    name: deploy
    groups: sudo
    shell: /bin/bash

- name: zapewnij zainstalowanie nginx
  apt:
    name: nginx
    state: present

Uruchom to na świeżej maszynie, a tworzy użytkownika i instaluje nginx; uruchom ponownie, a oba zadania raportują ok (nie changed) i nie robią nic. Ta idempotencja czyni playbook bezpiecznym do wielokrotnego uruchamiania — do zbiegania nowego serwera, zastosowania zmiany na istniejących albo po prostu udowodnienia, że rzeczywistość wciąż pasuje do twojego kodu. Każde zadanie raportuje ok, changed lub failed, więc przebieg jest też audytem: liczba changed mówi dokładnie, co zdryfowało od ostatniego razu.

Role: jednostka wielokrotnego użytku

Rola to samodzielny pakiet zadań, szablonów, plików i zmiennych dla jednego problemu — ruby, nginx, swap — ułożony w konwencjonalnej strukturze katalogów. Role są sposobem, w jaki sterta zadań staje się biblioteką reużywaną między projektami. Kilka, które trzymamy pod ręką:

Rola swap. Instancje chmurowe często przychodzą bez swapa, a aplikacja Ruby, która skoczy z pamięcią, zostanie zabita przez OOM bez niego. Rola tworzy swapfile, idempotentnie:

- name: utwórz swapfile
  command: fallocate -l {{ swap_size }} /swapfile
  args:
    creates: /swapfile          # magia: pomiń, jeśli /swapfile już istnieje
- name: sformatuj swapfile
  command: mkswap /swapfile
  args:
    creates: /swapfile.formatted
- name: włącz swap
  command: swapon /swapfile
  when: ansible_swaptotal_mb < 1

Argument creates: to idiom czyniący surowy command idempotentnym — mówi Ansible, by pominął zadanie, jeśli plik już istnieje, więc nieidempotentna komenda powłoki staje się bezpieczna do ponownego uruchomienia.

Rola ruby. Zainstaluj znane Ruby (przez rbenv albo ruby-install), przypnij wersję zmienną i połóż zależności do budowania potrzebne natywnym gemom:

- name: zainstaluj zależności do budowania
  apt:
    name: "{{ item }}"
    state: present
  loop: [build-essential, libpq-dev, libssl-dev, zlib1g-dev]

- name: zainstaluj ruby {{ ruby_version }}
  command: "rbenv install -s {{ ruby_version }}"
  become_user: deploy

Wersja żyje w zmiennej, więc upgrade Ruby w całej flocie to jednolinijkowa zmiana w pliku zmiennych plus przebieg playbooka.

Rola sshd. Hartowanie serwera, które chcesz mieć identyczne wszędzie: wyłącz logowanie roota, wyłącz uwierzytelnianie hasłem i przeładuj demona tylko, gdy konfiguracja faktycznie się zmieni — używając handlera:

- name: zahartuj sshd
  template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
  notify: restart sshd      # odpala handler, ale tylko przy zmianie

# handlers/main.yml
- name: restart sshd
  service:
    name: ssh
    state: restarted

Handlery to odpowiedź Ansible na „zrób tę kosztowną rzecz tylko, jeśli coś się zmieniło”. Zadanie template renderuje sshd_config z szablonu Jinja2; jeśli (i tylko jeśli) wyrenderowany plik różni się od tego na dysku, notify wyzwala handler restart sshd na końcu przebiegu. Nigdy niepotrzebnie nie odbijasz demona SSH, a przebieg-no-op pozostaje prawdziwym no-op.

Zmienne i szablony trzymają to DRY

Szablony Jinja2 plus zmienne są tym, co powstrzymuje cię od kopiowania niemal identycznej konfiguracji między środowiskami. Konfiguracja nginx, połączenie z bazą, liczba workerów — wszystko renderowane ze zmiennych różniących się na grupę lub na host. Twoje serwery staging i produkcji uruchamiają te same role, parametryzowane różnymi zmiennymi — w ten sposób gwarantujesz, że faktycznie są podobne, zamiast mieć na to nadzieję.

Gdzie pasuje

Ansible nie jest odpowiedzią na wszystko. Pięknie provisionuje i konfiguruje działające maszyny; nie jest orkiestratorem do ich uruchamiania (to zadanie API chmury albo Terraforma) i nie jest zbudowany pod nieustannie wirujący świat kontenerów nadchodzący teraz. Ale dla bardzo częstego przypadku — zamiany świeżej maszyny Ubuntu w poprawnie skonfigurowany, zahartowany, odtwarzalny serwer aplikacji Ruby i trzymania małej ich floty w synchronizacji — to najprzyjemniejsze narzędzie, jakiego używaliśmy. Model bezagentowy oznacza, że możesz wdrożyć je dziś po południu, idempotentne role oznaczają, że twoje serwery przestają być płatkami śniegu, a całość żyje w gicie, gdzie jej miejsce. Dla małego zespołu tę kombinację trudno pobić.