Ansible + Nette — první kroky

Dneska bych rád napsal něco o deploymentu, orchestraci, automatizaci a <dosaď co tě zrovna napadne>.

Ansible ve firmě nějakou dobu používáme a díky týmu lidí okolo jsem měl možnost vidět jaké můžou být opravdové benefity a jak to může ušetřit hodně času a starostí. Po přechodu z klasického FTP deploye se i přes řadu nástrojů, které tomu pomáhají, stane z nasazení nové verze aplikace s Ansiblem snad čirá radost. Dost ale superlativ a podíváme se blíž jak v tom něco udělat.

Disclaimer: Ansible funguje jen na Linuxu a MacOS a je potřeba SSH na cílovou mašinu. Před pár měsíci ho koupil Red Hat, tak snad zbývá jen doufat.

Disclaimer #2: Tento článek je docela omezenou a praktickou ukázkou jak vůbec začít, protože vím, jak mi to vše ze začátku přišlo složité. Je daleko jednodušší stavět na něčem hotovém. Každopádně koho zajímá teorie a oficiální zdroje, pak doporučuju oficiální dokumentaci, popřípadě seriál na root-u.

I když většinu dne píšu v pythonu, dneska si ukážeme použití na PHP aplikaci a to konkrétně na #nettefw projektu. Řekněme, že nás už nebaví pokaždé při deployi myslet na to, že smažeme cache, nebo jestli máme náhodou práva na souborech a vlastně bychom třeba chtěli zmáčknout pomyslné červené tlačítko a všechno by se udělalo samo.

Začneme zlehka, tedy prerekvizitami. Instalace je jednoduchá. Předpokladem je python a pip (což je něco jako composer pro python :)) a pak jen:

pip install ansible

Tím si vše nainstalujeme, můžeme ověřit verzi standardním přepínačem version. Měli bychom být někde ve verzi >=2.0 (v době psaní článku je poslední právě 2.0).

Ač nerad, tak začnu trošku teorií, abychom věděli co googlit, když se něco nepovede nebo spadne. Základem jsou playbook-y, což je vlastně yaml soubor, který říká co chceme dělat. Máme v něm uvedené jednotlivé kroky, které chceme na daném stroji provést (task-y a nebo plays). Posledním dílkem do naší skládačky (né tak docela co se týče všech možností, ale v rámci rozsahu dnešního článku) je inventory. Inventář je soubor v ini formátu, kde definujeme cílové stroje a můžeme je dávat do skupin.

Začneme tedy tím, že si v rootu našeho projektu vytvoříme složku, například _ansible. V ní si pak vytvoříme pár souborů, prvním z nich bude soubor production a deploy.yml.

Pro jednodušší aplikaci většinou mám ansible přímo u kódu, je to pak jednodušší. V práci používáme samostatné repo.

První z nich (soubor production) je náš inventář. Říká, jaký bude náš cílový stroj(e). Jak jsem psal výše, tak je zapsán v ini formátu.

[webservers]
webserver1 10.10.10.11 ansible_ssh_user=root

Máme tedy stroj ve skupině webservers, který jsme pojmenovali webserver1. Dále specifikujeme IP adresu a také pod jakým uživatelem chceme tu celou magii provádět.

Druhý soubor, deploy.yml, pak bude říkat co se vlastně má provést. Pro začátek bychom chtěli nakopírovat adresář app na každý stroj ze skupiny webservers.

---
- name: deployment
hosts: webservers
tasks:
- name: synchronize app files
synchronize:
src: '{{ playbook_dir }}/../app'
dest: /tmp/nette_app
delete: yes
recursive: yes

A takhle vypadá celá naše magie. Když přeskočíme první řádek, tak máme vlastně seznam úkolů, který se jmenuje deployment, pak řekneme, že je aplikovatelný na stroj/stroje, které se jmenují nebo jsou ve skupině webservers. V praxi to znamená, že kdybychom do našeho inventáře přidali další záznam do stejné skupiny, Ansible pak automaticky provede tenhle seznam na oba servery. Hned potom už začíná výčet tasku. Náš první task říká, jak se jmenuje a taky, že používá modul synchronize.

Tady se trošku zastavím. V Ansible můžeme používat built-in moduly, které nám už řekněme abstrahují chování různých věcí/služeb/utilit. Celý seznam je tady a u každého modulu můžeme vidět jeho použití. Zápis je vlastně u všech stejný, a to sice:

<module_name>: <param1>=<val1>[ <param2>=<val2>]

Pokud bychom třeba chtěli použít příkaz rm -rf /tmp/cache, můžeme to (jak bývá zvykem) zapsat více způsoby.

- name: delete cache directory
shell: rm -rf /tmp/cache

Ale protože máme modul na práci s file systémem, zkusím tu druhou možnost, a to sice:

- name: delete cache directory
file: name=/tmp/cache state=absent

Když se teď znova podíváme na náš kód:

synchronize:
src: '{{ playbook_dir }}/../app'
dest: /tmp/nette_app
delete: yes
recursive: yes

Tak vlastně říkáme, že používáme modul synchronize a předáváme mu parametry src, dest, delete a recursive. Ještě tam máme jednu magickou věc a to je {{ playbook_dir }}. Ansible samořejmě podporuje možnost práce s proměnnými. Máme jich celou sadu built-in, jako je třeba distribuce cílového systému, jeho verze, nebo cesta k adresáři, odkud spouštím playbook. Můžeme si také registrovat/nastavovat vlastní, ale o tom jindy.

Takže máme první task, který nám automaticky synchronizuje adresář app. Abychom náš první playbook spustili, tak nám stačí do terminálu naťukat

ansible-playbook -i production deploy.yml

Což nám spustí playbook deploy.yml na inventáři production. Výstup pak bude vypadat takto:

Tip#1: Ansible nám umožňuje použít pár přepínačů navíc, které můžou pomoci.

  • -C checkmode — neprovede příkazy, ale ukáže výsledky
  • -v verbose mode — máme by default — člověk vidí, co se přesně děje

Jdeme dál, teď bychom potřebovali ješte deploynout adresář www. Abychom nemuseli duplikovat kód pro další adresář (DRY), použijeme konstrukci with_items

---
- name: deployment
hosts: webservers
tasks:
- name: synchronize app files
synchronize:
src: '{{ playbook_dir }}/../{{ item }}'
dest: /tmp/nette_app
delete: yes
recursive: yes
with_items:
- app
- www

Což udělá to samé, ale pro adresáře app a www, takže jsme znovu použili již napsaný kód. Výsledek pak

Pecka! Máme vlastně hotový deploy. Ještě tu máme ale cache složku, většinou se její obsah maže, takže to bychom mohli zapracovat. Navíc si ještě zkontrolujeme, že máme složku log, která je přítomná a zapisovatelná, tedy že má práva 777. Přidáme tedy další dva tasky

- name: log dir present and writable
file: state=directory path=/tmp/nette_app/log mode=0777

- name: cache directory cleared
shell:
rm -rfv /tmp/nette_app/cache/*

První řekne tedy, že cesta /tmp/nette_app/log má být přítomna s právy 0777. Druhá pak spustí shell command. Když se na to mrkneme, tak máme na 20 řádků docela pěkný (a hlavně čitelný a udržovatelný) deploy skript.

Nakonec si ukážeme ještě jednu věc a to sice, že bycom chtěli podmínit mazání cache tím, že nastane nějaká změna. Tedy pokud se něco změní v adresáři app, tak promažeme cache. K tomu budeme potřebovat znát dvě věci. První, že můžeme výsledek tasku zaregistrovat do nějaké proměnné, abychom ho mohli použít. A druhou, že můžeme vykonání nějakého tasku podmínit. Teď to jen dáme dohromady.

Do prvního tasku, kdy kopírujeme složky na server si přidáme jeden řádek, který nám zaregistruje výstup.

register: sync_out

Tak budeme mít dál k dispozici proměnnou sync_out, která obsahuje potřebné informace. Do posledního tasku, kde čistíme cache si zas přidáme podmínku, že ho provedeme, jen když se něco změní ve složce app.

when: sync_out.results.0.changed

Což říká, že se provede jen v případě, že 0-tý výsledek byl changed (tedy se něco změnilo). Celý kód pak vypadá takto

To je konec tohoto úvodu, příště se mrkneme jak pracovat s Composerem a deployovat budem přímo z git repozitáře, a jak funguje tagování. Kdyby byly nějaké otázky, pak klidně tady nebo Twitter. Díky!