Není to tak dávno, co jsem potřeboval napsat několik Ansible playbooků, abych připravil server pro nasazení aplikace Rails. A kupodivu jsem nenašel jednoduchý návod krok za krokem. Nechtěl jsem kopírovat učebnici někoho jiného, aniž bych pochopil, co se děje, a nakonec jsem musel číst dokumentaci a shromažďovat vše sám. Možná mohu někomu pomoci tento proces urychlit pomocí tohoto článku.
První věc, kterou je třeba pochopit, je, že ansible vám poskytuje pohodlné rozhraní pro provádění předdefinovaného seznamu akcí na vzdáleném serveru (serverech) prostřednictvím SSH. Není zde žádná magie, nemůžete nainstalovat plugin a získat nulový výpadek nasazení vaší aplikace s dockerem, monitorováním a dalšími vychytávkami hned po vybalení. Abyste mohli napsat playbook, musíte vědět, co přesně chcete dělat a jak to udělat. Proto se nespokojím s hotovými playbooky z GitHubu nebo články typu: „Zkopíruj a spusť, bude to fungovat.“
Co potřebujeme?
Jak jsem již řekl, abyste mohli napsat playbook, musíte vědět, co chcete dělat a jak to dělat. Pojďme se rozhodnout, co potřebujeme. Pro aplikaci Rails budeme potřebovat několik systémových balíčků: nginx, postgresql (redis, atd.). Kromě toho potřebujeme konkrétní verzi ruby. Nejlepší je nainstalovat přes rbenv (rvm, asdf...). Spouštět toto vše jako uživatel root je vždy špatný nápad, takže musíte vytvořit samostatného uživatele a nakonfigurovat jeho práva. Poté musíte nahrát náš kód na server, zkopírovat konfigurace pro nginx, postgres atd. a spustit všechny tyto služby.
V důsledku toho je sled akcí následující:
- Přihlaste se jako root
- nainstalovat systémové balíčky
- vytvořit nového uživatele, nakonfigurovat práva, klíč ssh
- nakonfigurujte systémové balíčky (nginx atd.) a spusťte je
- Vytvoříme uživatele v databázi (můžete okamžitě vytvořit databázi)
- Přihlaste se jako nový uživatel
- Nainstalujte rbenv a ruby
- Instalace svazku
- Nahrávání kódu aplikace
- Spuštění serveru Puma
Navíc poslední fáze lze provést pomocí capistrano, alespoň po vybalení umí zkopírovat kód do adresářů vydání, přepnout vydání pomocí symbolického odkazu po úspěšném nasazení, zkopírovat konfigurace ze sdíleného adresáře, restartovat pumu atd. To vše lze provést pomocí Ansible, ale proč?
Struktura souboru
Ansible má přísné
Jednoduchá příručka
Playbook je yml soubor, který pomocí speciální syntaxe popisuje, co má Ansible dělat a jak. Pojďme vytvořit první playbook, který nic nedělá:
---
- name: Simple playbook
hosts: all
Zde jednoduše říkáme, že se jmenuje náš playbook Simple Playbook
a že jeho obsah by měl být spuštěn pro všechny hostitele. Můžeme jej uložit do adresáře /ansible s názvem playbook.yml
a zkuste spustit:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible říká, že nezná žádné hostitele, kteří odpovídají seznamu všech. Musí být uvedeny ve zvláštním
Vytvoříme jej ve stejném ansible adresáři:
123.123.123.123
Takto jednoduše určíme hostitele (ideálně hostitele našeho VPS pro testování, nebo si můžete zaregistrovat localhost) a uložíme pod jménem inventory
.
Můžete zkusit spustit ansible se souborem inventáře:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Pokud máte ssh přístup k uvedenému hostiteli, pak se ansible připojí a shromáždí informace o vzdáleném systému. (výchozí TASK [Gathering Facts]), po kterém podá krátkou zprávu o provedení (PLAY RECAP).
Ve výchozím nastavení používá připojení uživatelské jméno, pod kterým jste přihlášeni do systému. S největší pravděpodobností to nebude na hostiteli. V souboru playbook můžete pomocí direktivy remote_user určit, kterého uživatele použít k připojení. Také informace o vzdáleném systému pro vás mohou být často zbytečné a neměli byste ztrácet čas jejich shromažďováním. Tuto úlohu lze také zakázat:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Zkuste spustit playbook znovu a ujistěte se, že připojení funguje. (Pokud jste zadali uživatele root, musíte také zadat direktivu se: true, abyste získali zvýšená práva. Jak je napsáno v dokumentaci: become set to ‘true’/’yes’ to activate privilege escalation.
i když není zcela jasné proč).
Možná se zobrazí chyba způsobená tím, že ansible nemůže určit interpret Pythonu, pak jej můžete zadat ručně:
ansible_python_interpreter: /usr/bin/python3
Kde máte python zjistíte pomocí příkazu whereis python
.
Instalace systémových balíčků
Standardní distribuce Ansible obsahuje mnoho modulů pro práci s různými systémovými balíčky, takže z žádného důvodu nemusíme psát bash skripty. Nyní potřebujeme jeden z těchto modulů k aktualizaci systému a instalaci systémových balíčků. Na svém VPS mám Ubuntu Linux, takže k instalaci balíčků používám apt-get
и
Doplňme naši příručku o první úkoly:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
tasks:
- name: Update system
apt: update_cache=yes
- name: Install system dependencies
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
Úloha je přesně ta úloha, kterou bude Ansible provádět na vzdálených serverech. Úkolu dáme jméno, abychom mohli sledovat jeho provedení v protokolu. A pomocí syntaxe konkrétního modulu popíšeme, co potřebuje udělat. V tomto případě apt: update_cache=yes
- říká aktualizovat systémové balíčky pomocí modulu apt. Druhý příkaz je trochu složitější. Předáme modul apt seznam balíčků a řekneme, že jsou state
by se měl stát present
, to znamená, že tyto balíčky nainstalujeme. Podobným způsobem jim můžeme říct, aby je smazali, nebo je aktualizovali pouhou změnou state
. Vezměte prosím na vědomí, že aby rails fungovaly s postgresql, potřebujeme balíček postgresql-contrib, který nyní instalujeme. Opět to musíte vědět a udělat to; ansible to sám o sobě neudělá.
Zkuste znovu spustit playbook a zkontrolujte, zda jsou balíčky nainstalovány.
Vytváření nových uživatelů.
Pro práci s uživateli má Ansible také modul – uživatel. Přidejme ještě jeden úkol (již známé části playbooku jsem schoval za komentáře, abych ho pokaždé nekopíroval celý):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Vytvoříme nového uživatele, nastavíme mu schéma a heslo. A pak narazíme na několik problémů. Co když se uživatelská jména musí lišit pro různé hostitele? A ukládat do playbooku heslo v čistém textu je velmi špatný nápad. Pro začátek dáme uživatelské jméno a heslo do proměnných a na konci článku ukážu, jak heslo zašifrovat.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
Proměnné se v sešitech nastavují pomocí dvojitých složených závorek.
Hodnoty proměnných uvedeme v inventárním souboru:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasd
Vezměte prosím na vědomí směrnici [all:vars]
- říká, že dalším blokem textu jsou proměnné (vars) a jsou použitelné pro všechny hostitele (všechny).
Zajímavý je i design "{{ user_password | password_hash('sha512') }}"
. Jde o to, že ansible neinstaluje uživatele přes user_add
jako byste to dělali ručně. A všechna data ukládá přímo, proto musíme heslo také předem převést na hash, což tento příkaz dělá.
Přidejme našeho uživatele do skupiny sudo. Předtím se však musíme ujistit, že taková skupina existuje, protože to za nás nikdo neudělá:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
Vše je celkem jednoduché, máme zde i skupinový modul pro vytváření skupin, se syntaxí velmi podobnou apt. Poté stačí tuto skupinu zaregistrovat uživateli (groups: "sudo"
).
Je také užitečné přidat tomuto uživateli ssh klíč, abychom se mohli přihlásit pomocí něj bez hesla:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Deploy SSH Key
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: present
V tomto případě je zajímavý design "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
— zkopíruje obsah souboru id_rsa.pub (vaše jméno se může lišit), tedy veřejnou část ssh klíče a nahraje jej do seznamu autorizovaných klíčů pro uživatele na serveru.
Role
Všechny tři úkoly pro tvorbu použití lze snadno zařadit do jedné skupiny úkolů a bylo by dobré tuto skupinu uložit odděleně od hlavního playbooku, aby se příliš nerozrostla. Pro tento účel má Ansible
Podle struktury souborů uvedené na samém začátku musí být role umístěny v samostatném adresáři rolí, pro každou roli existuje samostatný adresář se stejným názvem, uvnitř adresáře úkolů, souborů, šablon atd.
Vytvořme strukturu souborů: ./ansible/roles/user/tasks/main.yml
(main je hlavní soubor, který bude načten a spuštěn, když je role připojena k playbooku; lze k němu připojit další soubory rolí). Nyní můžete přenést všechny úkoly související s uživatelem do tohoto souboru:
# Create user and add him to groups
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Deploy SSH Key
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: present
V hlavní příručce musíte určit, že chcete použít uživatelskou roli:
---
- name: Simple playbook
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Update system
apt: update_cache=yes
- name: Install system dependencies
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
roles:
- user
Také může mít smysl aktualizovat systém před všemi ostatními úkoly; k tomu můžete blok přejmenovat tasks
ve kterém jsou definovány v pre_tasks
.
Nastavení nginx
Nginx bychom již měli mít nainstalovaný, musíme jej nakonfigurovat a spustit. Udělejme to hned v roli. Vytvořme strukturu souborů:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templates
Nyní potřebujeme soubory a šablony. Rozdíl mezi nimi je v tom, že ansible kopíruje soubory přímo, tak jak je. A šablony musí mít příponu j2 a mohou používat proměnné hodnoty pomocí stejných dvojitých složených závorek.
Povolme nginx main.yml
soubor. K tomu máme modul systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Zde nejen říkáme, že nginx musí být spuštěn (tedy jej spustíme), ale rovnou říkáme, že musí být povolen.
Nyní zkopírujeme konfigurační soubory:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
- name: Copy the nginx.conf
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
- name: Copy template my_app.conf
template:
src: my_app_conf.j2
dest: /etc/nginx/sites-available/my_app.conf
owner: root
group: root
mode: '0644'
Vytvoříme hlavní konfigurační soubor nginx (můžete si ho vzít přímo ze serveru nebo si ho napsat sami). A také konfigurační soubor pro naši aplikaci v adresáři sites_available (není to nutné, ale užitečné). V prvním případě použijeme ke kopírování souborů modul kopírování (soubor musí být in /ansible/roles/nginx/files/nginx.conf
). Ve druhém zkopírujeme šablonu a nahradíme hodnoty proměnných. Šablona by měla být in /ansible/roles/nginx/templates/my_app.j2
). A může to vypadat nějak takto:
upstream {{ app_name }} {
server unix:{{ app_path }}/shared/tmp/sockets/puma.sock;
}
server {
listen 80;
server_name {{ server_name }} {{ inventory_hostname }};
root {{ app_path }}/current/public;
try_files $uri/index.html $uri.html $uri @{{ app_name }};
....
}
Věnujte pozornost vložkám {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
— to jsou všechny proměnné, jejichž hodnoty Ansible dosadí do šablony před zkopírováním. To je užitečné, pokud používáte playbook pro různé skupiny hostitelů. Můžeme například přidat náš soubor inventáře:
[production]
123.123.123.123
[staging]
231.231.231.231
[all:vars]
user=my_user
user_password=123qweasd
[production:vars]
server_name=production
app_path=/home/www/my_app
app_name=my_app
[staging:vars]
server_name=staging
app_path=/home/www/my_stage
app_name=my_stage_app
Pokud nyní spustíme naši příručku, provede zadané úkoly pro oba hostitele. Zároveň se však pro hostitele inscenace budou proměnné lišit od produkčních, a to nejen v rolích a playbookech, ale také v konfiguracích nginx. {{ inventory_hostname }}
není třeba uvádět v inventárním souboru - toto
Pokud chcete mít soubor inventáře pro několik hostitelů, ale spustit pouze pro jednu skupinu, lze to provést pomocí následujícího příkazu:
ansible-playbook -i inventory ./playbook.yml -l "staging"
Další možností je mít samostatné soubory inventáře pro různé skupiny. Nebo můžete oba přístupy zkombinovat, pokud máte mnoho různých hostitelů.
Vraťme se k nastavení nginx. Po zkopírování konfiguračních souborů musíme vytvořit symbolický odkaz v sitetest_enabled na my_app.conf z sites_available. A restartujte nginx.
... # old code in mail.yml
- name: Create symlink to sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: restart nginx
service:
name: nginx
state: restarted
Zde je vše jednoduché - opět ansible moduly s celkem standardní syntaxí. Ale je tu jeden bod. Nemá smysl pokaždé restartovat nginx. Všimli jste si, že nepíšeme příkazy jako: „udělej to takhle“, syntaxe vypadá spíše jako „toto by mělo mít tento stav“. A nejčastěji přesně takto funguje ansible. Pokud skupina již existuje nebo je již nainstalován systémový balíček, pak to ansible zkontroluje a přeskočí úlohu. Také soubory nebudou zkopírovány, pokud zcela odpovídají tomu, co je již na serveru. Můžeme toho využít a restartovat nginx pouze v případě, že byly změněny konfigurační soubory. Na to existuje směrnice o registraci:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
- name: Copy the nginx.conf
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
register: restart_nginx
- name: Copy template my_app.conf
template:
src: my_app_conf.j2
dest: /etc/nginx/sites-available/my_app.conf
owner: root
group: root
mode: '0644'
register: restart_nginx
- name: Create symlink to sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: restart nginx
service:
name: nginx
state: restarted
when: restart_nginx.changed
Pokud se některý z konfiguračních souborů změní, bude vytvořena kopie a proměnná bude zaregistrována restart_nginx
. A pouze pokud byla tato proměnná zaregistrována, bude služba restartována.
A samozřejmě musíte přidat roli nginx do hlavní příručky.
Nastavení postgresql
Musíme povolit postgresql pomocí systemd stejným způsobem, jako jsme to udělali s nginx, a také vytvořit uživatele, kterého budeme používat pro přístup k databázi a databázi samotné.
Pojďme vytvořit roli /ansible/roles/postgresql/tasks/main.yml
:
# Create user in postgresql
- name: enable postgresql and start
systemd:
name: postgresql
state: started
enabled: yes
- name: Create database user
become_user: postgres
postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
role_attr_flags: SUPERUSER
- name: Create database
become_user: postgres
postgresql_db:
name: "{{ db_name }}"
encoding: UTF-8
owner: "{{ db_user }}"
Nebudu popisovat, jak přidat proměnné do inventáře, to se již dělalo mnohokrát, stejně jako syntaxe modulů postgresql_db a postgresql_user. Více informací naleznete v dokumentaci. Zde je nejzajímavější směrnice become_user: postgres
. Faktem je, že standardně má k databázi postgresql přístup pouze uživatel postgres a pouze lokálně. Tato směrnice nám umožňuje provádět příkazy jménem tohoto uživatele (pokud máme samozřejmě přístup).
Také možná budete muset přidat řádek do pg_hba.conf, abyste umožnili novému uživateli přístup k databázi. To lze provést stejným způsobem, jako jsme změnili konfiguraci nginx.
A samozřejmě je potřeba přidat postgresql roli do hlavního playbooku.
Instalace ruby přes rbenv
Ansible nemá moduly pro práci s rbenv, ale instaluje se naklonováním git repozitáře. Proto se tento problém stává nejvíce nestandardním. Vytvořme pro ni roli /ansible/roles/ruby_rbenv/main.yml
a začneme to vyplňovat:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv
Pro práci pod uživatelem, kterého jsme pro tyto účely vytvořili, opět používáme direktivu Staň se_uživatelem. Protože rbenv je nainstalován ve svém domovském adresáři, a ne globálně. A také používáme modul git ke klonování úložiště s uvedením repo a dest.
Dále musíme zaregistrovat rbenv init v bashrc a přidat rbenv do PATH tam. K tomu máme modul lineinfile:
- name: Add rbenv to PATH
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'export PATH="${HOME}/.rbenv/bin:${PATH}"'
- name: Add rbenv init to bashrc
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'eval "$(rbenv init -)"'
Poté musíte nainstalovat ruby_build:
- name: Install ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build
A nakonec nainstalujte ruby. To se provádí pomocí rbenv, tedy jednoduše pomocí příkazu bash:
- name: Install ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: /bin/bash
Říkáme, který příkaz provést a čím. Zde však narážíme na skutečnost, že ansible před spuštěním příkazů nespustí kód obsažený v bashrc. To znamená, že rbenv bude muset být definováno přímo ve stejném skriptu.
Další problém je způsoben tím, že příkaz shell nemá z ansible hlediska žádný stav. To znamená, že nedojde k automatické kontrole, zda je tato verze ruby instalovaná nebo ne. Můžeme to udělat sami:
- name: Install ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
if ! rbenv versions | grep -q {{ ruby_version }}
then rbenv install {{ ruby_version }} && rbenv global {{ ruby_version }}
fi
args:
executable: /bin/bash
Zbývá pouze nainstalovat bundler:
- name: Install bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundler
A znovu přidejte naši roli ruby_rbenv do hlavní příručky.
Sdílené soubory.
Obecně lze nastavení dokončit zde. Dále zbývá pouze spustit capistrano a to si kód zkopíruje samo, vytvoří potřebné adresáře a spustí aplikaci (pokud je vše správně nakonfigurováno). Capistrano však často vyžaduje další konfigurační soubory, jako např database.yml
nebo .env
Lze je kopírovat stejně jako soubory a šablony pro nginx. Je tu jen jedna jemnost. Před kopírováním souborů pro ně musíte vytvořit adresářovou strukturu, asi takto:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directory
zadáme pouze jeden adresář a ansible v případě potřeby automaticky vytvoří nadřazené.
Ansible Vault
Již jsme se setkali s tím, že proměnné mohou obsahovat tajná data, jako je heslo uživatele. Pokud jste vytvořili .env
soubor pro aplikaci a database.yml
pak musí být takových kritických dat ještě více. Bylo by dobré je skrýt před zvědavými pohledy. K tomuto účelu se používá
Vytvořme soubor pro proměnné /ansible/vars/all.yml
(zde můžete vytvořit různé soubory pro různé skupiny hostitelů, stejně jako v souboru inventáře: production.yml, staging.yml atd.).
Všechny proměnné, které musí být zašifrovány, musí být přeneseny do tohoto souboru pomocí standardní syntaxe yml:
# System vars
user_password: 123qweasd
db_password: 123qweasd
# ENV vars
aws_access_key_id: xxxxx
aws_secret_access_key: xxxxxx
aws_bucket: bucket_name
rails_secret_key_base: very_secret_key_base
Poté lze tento soubor zašifrovat příkazem:
ansible-vault encrypt ./vars/all.yml
Při šifrování budete samozřejmě muset nastavit heslo pro dešifrování. Po zavolání tohoto příkazu můžete vidět, co bude uvnitř souboru.
Prostřednictvím ansible-vault decrypt
soubor lze dešifrovat, upravit a poté znovu zašifrovat.
Pro práci není nutné soubor dešifrovat. Uložíte jej zašifrovaný a spustíte playbook s argumentem --ask-vault-pass
. Ansible požádá o heslo, načte proměnné a provede úkoly. Všechna data zůstanou zašifrována.
Kompletní příkaz pro několik skupin hostitelů a ansible vault bude vypadat nějak takto:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
Ale neposkytnu vám plné znění herních knih a rolí, napište si to sami. Protože ansible je takový – pokud nerozumíte tomu, co je třeba udělat, pak to za vás neudělá.
Zdroj: www.habr.com