Нещодавно мені було необхідно написати кілька ansible playbooks для підготовки сервера до деплою rails програми. І, напрочуд, я не знайшов простого покрокового мануалу. Копіювати чужий плейбук без розуміння того, що відбувається, я не хотів і в результаті довелося читати документацію, збираючи все самостійно. Можливо, комусь я зможу допомогти цей процес прискорити за допомогою цієї статті.
Насамперед варто розуміти, що ansible надає вам зручний інтерфейс для виконання заздалегідь визначеного списку дій на віддаленому сервері через SSH. Тут немає ніякої магії, не можна поставити плагін і отримати з коробки zero downtime деплою своєї програми з докером, моніторингом та іншими плюшками. Для того щоб написати плейбук ви повинні знати, що саме ви хочете зробити і як це зробити. Тому мене не влаштовують готові плейбуки з гітхабу, або статті виду: "Скопіюйте та запустіть, буде працювати".
Що нам потрібно?
Як я вже казав, щоб написати плейбук треба знати, що ви хочете зробити і як це зробити. Давайте визначимося з тим, що нам потрібне. Для Rails програми нам знадобиться кілька системних пакетів: nginx, postgresql (redis, etc). Крім цього нам потрібний ruby певної версії. Ставити його найкраще через rbenv (rvm, asdf…). Запускати все це з-під root користувача - завжди погана ідея, тому треба створити окремого користувача, і налаштувати йому права. Після цього необхідно залити код на сервер, скопіювати конфіги для nginx, postgres, etc і запустити всі ці сервіси.
У результаті послідовність дій така:
- Логін під рутом
- встановлюємо системні пакети
- створюємо нового користувача, налаштовуємо права, ssh ключ
- налаштовуємо системні пакети (nginx etc) та запускаємо їх
- Створюємо користувача в БД (можна одразу і базу створити)
- Логинимся новим користувачем
- Встановлюємо rbenv та ruby
- Встановлюємо бандлер
- Заливаємо код програми
- Запускаємо Puma сервер
Причому останні етапи можна робити за допомогою capistrano, принаймні вона з коробки вміє копіювати код у релізні директорії, перемикати реліз симлінком при успішному деплої, копіювати конфіги з shared директорії, рестартувати puma і т.д. Все це можна зробити за допомогою Ansible, але навіщо?
Файлова структура
Ansible має строгу
Simple Playbook
Playbook - це yml файл, в якому за допомогою спеціального синтаксису описано, що і як ansible має зробити. Давайте створимо перший плейбук, який нічого не робить:
---
- name: Simple playbook
hosts: all
Тут ми просто говоримо, що наш playbook називається Simple Playbook
і що виконуватись його вміст має для всіх хостів. Ми можемо зберегти його в /ansible директорії з ім'ям playbook.yml
і спробувати запустити:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible каже, що не знає хостів, які б відповідали списку all. Їх треба перерахувати у спеціальному
Давайте створимо його в тій же ansible директорії:
123.123.123.123
Ось так просто вказуємо хост (в ідеалі хост свого VPS для тестів, або можна localhost прописати) і зберігаємо його під ім'ям inventory
.
Можна спробувати запустити ansible з invetory файлом:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Якщо у вас є доступ до ssh до вказаного хоста, то ansible підключиться і збере інформацію про віддалену систему. (Дефолтний TASK [Gathering Facts] ) після чого дасть короткий звіт про виконання (PLAY RECAP).
За замовчуванням для з'єднання використовується ім'я користувача, під яким ви залогінені в системі. На хості його, швидше за все, не буде. У playbook файлі можна вказати, якого користувача використовувати для підключення за допомогою директиви remote_user. Також інформація про віддалену систему вам часто може бути непотрібна і не варто витрачати час на її збір. Це завдання також можна вимкнути:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Спробуйте ще раз запустити playbook і переконатися, що з'єднання працює. (Якщо ви вказали root користувача, то так само треба вказати директиву become: true, щоб отримати підвищені права. Як написано в документації: become set to ‘true’/’yes’ to activate privilege escalation.
хоча не зовсім зрозуміло, навіщо).
Можливо ви отримаєте помилку, викликану тим, що ansible не може визначити інтерпретатор пітона, тоді його можна вказати вручну:
ansible_python_interpreter: /usr/bin/python3
де у вас лежить python можна дізнатися командою whereis python
.
Встановлення системних пакетів
У стандартній поставці Ansible входить безліч модулів для роботи з різними системними пакетами, завдяки чому нам не доводиться з будь-якого приводу писати bash скрипти. Зараз нам знадобиться один із таких модулів для оновлення системи та встановлення системних пакетів. У мене на VPS стоїть Ubuntu Linux відповідно для встановлення пакетів, я використовую apt-get
и
Доповнимо наш плейбук першими завданнями:
---
- 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
Task - це якраз завдання, яке ansible буде виконувати на віддалених серверах. Ми даємо задачі ім'я, щоб відстежувати її виконання у лозі. І описуємо за допомогою синтаксису конкретного модуля, що йому потрібно зробити. В даному випадку apt: update_cache=yes
- каже оновити пакети системи за допомогою модуля apt. Друга команда дещо складніша. Ми передаємо в модуль apt список пакетів, і кажемо, що їх state
повинен стати present
, тобто говоримо встановити ці пакети. Схожим чином, ми можемо сказати їх видалити, або оновити, просто змінивши state
. Зауважте, що для роботи rails з postgresql нам потрібен пакет postgresql-contrib, який ми зараз встановлюємо. Про це знову ж таки треба знати і зробити, а сам сам по собі цього робити не буде.
Спробуйте запустити playbook ще раз і перевірити, чи пакети встановляться.
Створення нових користувачів.
Для роботи з користувачами у Ansible також є модуль - user. Додамо ще один task (я приховав вже відомі частини плейбука за коментарями, щоб не копіювати його повністю щоразу):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Ми створюємо нового користувача, встановлюємо йому schell та пароль. І відразу стикаємося з кількома проблемами. Що якщо імена користувачів повинні бути різними для різних хостів? Та й зберігати пароль у відкритому вигляді у плейбуку дуже погана ідея. Для початку винесемо ім'я користувача та пароль у змінні, а ближче до кінця статті я покажу як пароль зашифрувати.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
за допомогою подвійних фігурних дужок у плейбуках встановлюються змінні.
Значення змінних ми вкажемо в файлі inventory:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasd
Зверніть увагу на директиву [all:vars]
- Вона говорить про те, що наступний блок тексту - це змінні (vars) і вони застосовні для всіх хостів (all).
Також цікава конструкція "{{ user_password | password_hash('sha512') }}"
. Справа в тому, що ansible не встановлює користувача через user_add
як ви би це вручну. А зберігає всі дані безпосередньо, через що пароль ми також повинні заздалегідь перетворити на хеш, що робить дана команда.
Давайте додамо нашого користувача до групи sudo. Однак перед цим необхідно переконатися, що така група є тому, що за нас цього ніхто робити не буде:
---
- 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"
Все досить просто, у нас є модуль group для створення груп, з синтаксисом дуже схожим на apt. Після цього достатньо прописати цю групу користувачеві (groups: "sudo"
).
Також корисно додати цьому користувачеві ssh ключ, щоб ми могли логінуватися під ним без пароля:
---
- 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
У цьому випадку цікава конструкція "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
- вона копіює вміст файлу id_rsa.pub (у вас назва може і відрізнятися), тобто публічну частину ssh ключа і завантажує його в список авторизованих ключів для користувача на сервер.
ролі
Всі три завдання для створення користуватися можна легко віднести до однієї групи завдань, і непогано було б зберігати цю групу окремо від основного плейбука, щоб він не розростався. Для цього в ansible існують
Відповідно до зазначеної на самому початку файлової структури, ролі необхідно покласти в окрему директорію roles, для кожної ролі - окрема директорія з аналогічною назвою, всередині директорії tasks, files, templates, etc
Створимо файлову структуру: ./ansible/roles/user/tasks/main.yml
(main - це основний файл, який буде підвантажуватися і виконуватися при підключенні ролі до плейбука, в ньому можна підключати інші файли ролі). Тепер можна перенести в цей файл всі завдання, що стосуються користувача:
# 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
В основному ж плейбуку необхідно вказати використати роль user:
---
- 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
Так само, можливо, має сенс виконувати оновлення системи раніше всіх інших завдань, для цього можна перейменувати блок tasks
в якому вони визначені pre_tasks
.
Налаштування nginx
Nginx у нас має бути вже встановлений, необхідно його конфігурувати та запустити. Давайте робити це одразу в ролі. Створюємо файлову структуру:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templates
Тепер нам знадобляться файли та шаблони. Різниця між ними в тому, що файли ansible копіює безпосередньо, як є. А шаблони повинні мати розширення j2 і в них можна використовувати значення змінних за допомогою тих же подвійних фігурних дужок.
Давайте увімкнемо nginx в main.yml
файл. Для цього ми маємо модуль systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Тут ми не тільки говоримо, що nginx повинен бути started (тобто запускаємо його), але відразу говоримо, що він повинен бути enabled.
Тепер скопіюємо конфігураційні файли:
# 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'
Ми створюємо основний конфігураційний файл nginx (можна взяти його з сервера, або написати самостійно). І так само конфігураційний файл для нашої програми в sites_available директоорію (це не обов'язково, але корисно). У першому випадку ми використовуємо модуль copy для копіювання файлів (файл повинен лежати в /ansible/roles/nginx/files/nginx.conf
). У другому - копіюємо шаблон, підставляючи значення змінних. Шаблон повинен лежати в /ansible/roles/nginx/templates/my_app.j2
). І виглядати він може приблизно так:
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 }};
....
}
Зверніть увагу на вставки {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
— це всі змінні, значення яких ansible підставить шаблон перед копіюванням. Це корисно, якщо використовувати плейбук для різних груп хостів. Наприклад, ми можемо доповнити наш inventory файл:
[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
Якщо ми тепер запустимо наш плейбук, то він виконає зазначені завдання для обох хостів. Але при цьому для staging хоста змінні відрізнятимуться від production, і не тільки в ролях і плейбуках, а й у конфігах nginx. {{ inventory_hostname }}
не треба вказувати в файлі inventory — це
Якщо ви хочете мати inventory файл для декількох хостів, а запускати тільки для однієї групи, це можна зробити наступною командою:
ansible-playbook -i inventory ./playbook.yml -l "staging"
Інший варіант - мати окремі inventory файли для різних груп. Або можна комбінувати два підходи, якщо у вас багато розниз хостів.
Повернемося до налаштування nginx. Після копіювання конфігураційних файлів нам необхідно створити симлінк у sitest_enabled на my_app.conf із sites_available. І перезапустити 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
Тут все просто - знову модулі ansible з досить стандартним синтаксисом. Але є один момент. Перезапускати nginx щоразу немає сенсу. Ви звернули увагу, що ми не пишемо команди виду: «зробити ось це ось так», синтаксис виглядає швидше як «ось у цього має бути такий стан». І найчастіше саме так ansible і працює. Якщо група вже існує, або системний пакет вже встановлений, то ansible перевірить це і пропустить завдання. Також файли не копіюватимуться, якщо вони повністю збігаються з тим, що вже є на сервері. Ми можемо скористатися і перезапускати nginx тільки якщо конфігураційні файли були змінені. Для цього існує директива register:
# 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
Якщо один із конфігураційних файлів змінюється, то буде виконано копіювання та зареєстрована змінна restart_nginx
. І тільки якщо ця змінна була зареєстрована, перезапуск сервісу.
Ну і, звичайно, потрібно додати роль nginx до основної playbook.
Налаштування postgresql
Нам необхідно включити postgresql за допомогою systemd так само, як ми це робили з nginx, а також створити користувача, якого ми будемо використовувати для доступу до бази даних і саму базу даних.
Створимо роль /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 }}"
Я не розписуватиму, як додавати змінні в inventory, це вже робилося багато разів, так само як і синтаксис модулів postgresql_db і postgresql_user. Більше даних можна знайти у документації. Тут найцікавіша директива become_user: postgres
. Справа в тому, що за умовчанням доступ до бази postgresql є тільки у користувача postgres і тільки локально. Ця директива дозволяє нам виконувати команди від імені цього користувача (якщо звичайно ми маємо доступ).
Також, можливо, вам доведеться дописати рядок в pg_hba.conf щоб відкрити доступ новому користувачеві до бази. Це можна зробити так, як ми змінювали конфіг nginx.
Ну і звичайно треба додати роль postgresql в основний плейбук.
Встановлення ruby через rbenv
У ansible немає модулів для роботи з rbenv, а встановлюється він шляхом клонування git репозиторію. Тому це завдання стає найнестандартнішою. Створимо для неї роль /ansible/roles/ruby_rbenv/main.yml
і почнемо її заповнювати:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv
Ми знову використовуємо директиву become_user щоб працювати з-під створеного нами для цих цілей користувача. Так як rbenv встановлюється у його home директорії, а не глобально. І так само ми використовуємо модуль git для того, щоб схилювати репозиторій, вказуючи repo і dest.
Далі нам необхідно прописати rbenv init в bashrc і там же додати rbenv в PATH. Для цього у нас є модуль 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 -)"'
Після цього потрібно встановити ruby_build:
- name: Install ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build
І, нарешті, встановити ruby. Це робиться через rbenv, тобто просто 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
Ми говоримо, яку команду виконати та чим. Однак тут ми натрапимо на те, що ansible не запускає код, що міститься в bashrc перед запуском команд. Отже, rbenv доведеться визначати прямо в цьому ж скрипті.
Наступна проблема пов'язана з тим, що команда shell не має стану з точки зору ansible. Тобто автоматичної перевірки, чи встановлена ця версія ruby чи ні — не буде. Ми можемо зробити це самостійно:
- 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
І залишається встановити bundler:
- name: Install bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundler
І знову ж таки додати нашу роль ruby_rbenv в основний плейбук.
Спільні файли.
Загалом на цьому налаштування можна було б закінчити. Далі залишається запустити capistrano і воно саме скопіює код, створить потрібні каталоги і запустить програму (якщо все налаштовано правильно). Однак часто capistrano необхідні додаткові конфігураційні файли, такі як database.yml
або .env
Їх можна скопіювати так само як файли і шаблони для nginx. Є лише одна тонкість. Перед копіюванням файлів необхідно створити для них структуру каталогів, щось на зразок такого:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directory
ми вказуємо тільки одну директорію та ansible автоматично створить батьківські, якщо потрібно.
Ansible Vault
Ми вже натикалися на те, що в змінних можуть бути секретні дані такі як пароль користувача. Якщо ви створили .env
файл для програми, та database.yml
то там має бути ще більше таких критичних даних. Їх добре було б приховати від сторонніх очей. Для цього використовується
Створимо файл для пермінних /ansible/vars/all.yml
(Тут можна створювати різні файли для різних груп хостів, так само як в файлі inventory: production.yml, staging.yml, etc).
У цей файл необхідно перенести всі змінні, які мають бути зашифровані за допомогою стандартного 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
Після цього цей файл можна зашифрувати командою:
ansible-vault encrypt ./vars/all.yml
При шифруванні необхідно буде встановити пароль для дешифрування. Ви можете подивитися, що буде всередині файлу після виклику цієї команди.
За допомогою ansible-vault decrypt
файл можна розшифрувати, змінити і потім знову зашифрувати.
Для роботи розшифровувати файл не потрібно. Ви зберігаєте його у зашифрованому вигляді та запускаєте playbook з аргументом --ask-vault-pass
. Ansible запитає пароль, дістане змінні та виконає завдання. Усі дані залишаться зашифрованими.
Повністю команда для кількох груп хостів та ansible vault буде виглядати приблизно так:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
А повний текст плейбуків та ролей я вам не дам, пишіть самі. Тому що ansible штука така - якщо не розумієш, що треба зробити, то і він тобі не зробить.
Джерело: habr.com