Неотдавна трябваше да напиша няколко Ansible playbooks, за да подготвя сървъра за внедряване на Rails приложение. И, изненадващо, не намерих просто ръководство стъпка по стъпка. Не исках да копирам нечия друга книга, без да разбирам какво се случва, и в крайна сметка трябваше да прочета документацията, събирайки всичко сам. Може би мога да помогна на някого да ускори този процес с помощта на тази статия.
Първото нещо, което трябва да разберете е, че ansible ви предоставя удобен интерфейс за извършване на предварително дефиниран списък от действия на отдалечен сървър(и) чрез SSH. Тук няма магия, не можете да инсталирате плъгин и да получите внедряване на вашето приложение без прекъсване с докер, мониторинг и други екстри. За да напишете книга-игра, трябва да знаете какво точно искате да направите и как да го направите. Ето защо не съм доволен от готови книги за игри от GitHub или статии като: „Копирай и стартирай, ще работи.“
Какво ни трябва?
Както вече казах, за да напишете книга-игра трябва да знаете какво искате да направите и как да го направите. Да решим какво ни трябва. За Rails приложение ще ни трябват няколко системни пакета: nginx, postgresql (redis и т.н.). Освен това се нуждаем от специфична версия на ruby. Най-добре е да го инсталирате чрез rbenv (rvm, asdf...). Изпълнението на всичко това като root потребител винаги е лоша идея, така че трябва да създадете отделен потребител и да конфигурирате неговите права. След това трябва да качите нашия код на сървъра, да копирате конфигурациите за nginx, postgres и т.н. и да стартирате всички тези услуги.
В резултат на това последователността от действия е следната:
- Влезте като root
- инсталирайте системни пакети
- създайте нов потребител, конфигурирайте права, ssh ключ
- конфигурирайте системни пакети (nginx и т.н.) и ги стартирайте
- Създаваме потребител в базата данни (можете веднага да създадете база данни)
- Влезте като нов потребител
- Инсталирайте rbenv и ruby
- Инсталиране на пакета
- Качване на кода на приложението
- Стартиране на Puma сървър
Освен това, последните етапи могат да бъдат направени с помощта на capistrano, поне извън кутията той може да копира код в директории за издаване, да превключва изданието със символна връзка при успешно разгръщане, да копира конфигурации от споделена директория, да рестартира puma и т.н. Всичко това може да се направи с помощта на Ansible, но защо?
Файлова структура
Ansible има строг
Проста книга за игра
Playbook е yml файл, който, използвайки специален синтаксис, описва какво трябва да прави Ansible и как. Нека създадем първата книга-игра, която не прави нищо:
---
- name: Simple playbook
hosts: all
Тук просто казваме, че нашата книга с игри се нарича Simple Playbook
и че съдържанието му трябва да се изпълни за всички хостове. Можем да го запазим в директория /ansible с името playbook.yml
и опитайте да стартирате:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible казва, че не знае нито един хост, който да съответства на списъка с всички. Те трябва да бъдат изброени в специален
Нека го създадем в същата ansible директория:
123.123.123.123
Ето как ние просто посочваме хоста (в идеалния случай хоста на нашия VPS за тестване или можете да регистрирате localhost) и го запазваме под името inventory
.
Можете да опитате да стартирате ansible с файл с инвентар:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Ако имате ssh достъп до посочения хост, тогава ansible ще се свърже и ще събере информация за отдалечената система. (ЗАДАЧА по подразбиране [Събиране на факти]), след което ще даде кратък отчет за изпълнението (PLAY RECAP).
По подразбиране връзката използва потребителското име, под което сте влезли в системата. Най-вероятно няма да е на хоста. Във файла на книгата можете да посочите кой потребител да използвате за свързване с помощта на директивата remote_user. Освен това информацията за отдалечена система често може да ви е ненужна и не трябва да губите време за нейното събиране. Тази задача може също да бъде деактивирана:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Опитайте отново да стартирате книгата и се уверете, че връзката работи. (Ако сте посочили root потребителя, тогава трябва също да посочите директивата become: true, за да получите повишени права. Както е написано в документацията: become set to ‘true’/’yes’ to activate privilege escalation.
въпреки че не е напълно ясно защо).
Може би ще получите грешка, причинена от факта, че ansible не може да определи интерпретатора на Python, тогава можете да го посочите ръчно:
ansible_python_interpreter: /usr/bin/python3
Можете да разберете къде имате python с командата whereis python
.
Инсталиране на системни пакети
Стандартната дистрибуция на Ansible включва много модули за работа с различни системни пакети, така че не се налага да пишем bash скриптове по някаква причина. Сега имаме нужда от един от тези модули, за да актуализираме системата и да инсталираме системни пакети. Имам Ubuntu Linux на моя VPS, така че използвам за инсталиране на пакети 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
. Моля, обърнете внимание, че за да работят релсите с postgresql, се нуждаем от пакета postgresql-contrib, който инсталираме сега. Отново, трябва да знаете и да направите това; ansible сам по себе си няма да направи това.
Опитайте да стартирате отново книгата и проверете дали пакетите са инсталирани.
Създаване на нови потребители.
За работа с потребители Ansible разполага и с модул - потребител. Нека добавим още една задача (скрих вече познатите части от книгата зад коментарите, за да не я копирам изцяло всеки път):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Създаваме нов потребител, задаваме схема и парола за него. И тогава се натъкваме на няколко проблема. Ами ако потребителските имена трябва да са различни за различните хостове? И съхраняването на паролата в ясен текст в книгата е много лоша идея. Като начало, нека поставим потребителското име и паролата в променливи, а към края на статията ще покажа как да криптирате паролата.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
Променливите се задават в книгите с помощта на двойни фигурни скоби.
Ще посочим стойностите на променливите във файла с инвентара:
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"
Всичко е доста просто, имаме и групов модул за създаване на групи, със синтаксис, много подобен на 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 има
Съгласно файловата структура, посочена в самото начало, ролите трябва да бъдат поставени в отделна директория за роли, за всяка роля има отделна директория със същото име, вътре в директорията задачи, файлове, шаблони и т.н.
Нека създадем файлова структура: ./ansible/roles/user/tasks/main.yml
(main е основният файл, който ще бъде зареден и изпълнен, когато дадена роля е свързана с playbook; други ролеви файлове могат да бъдат свързани към него). Сега можете да прехвърлите всички задачи, свързани с потребителя, в този файл:
# 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
В основната инструкция трябва да посочите да използвате потребителската роля:
---
- 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
файл. За това имаме системен модул:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Тук не само казваме, че nginx трябва да бъде стартиран (т.е. стартираме го), но веднага казваме, че трябва да бъде активиран.
Сега нека копираме конфигурационните файлове:
# 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 (това не е необходимо, но полезно). В първия случай използваме модула за копиране, за да копираме файлове (файлът трябва да е в /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 ще замени в шаблона преди копиране. Това е полезно, ако използвате книга за игра за различни групи хостове. Например, можем да добавим нашия файл с инвентара:
[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
Ако сега стартираме нашата книга за игра, тя ще изпълни посочените задачи и за двата хоста. Но в същото време, за етапен хост, променливите ще бъдат различни от производствените и не само в ролите и книгите за игри, но и в конфигурациите на nginx. {{ inventory_hostname }}
не е необходимо да се посочват в инвентарния файл – това
Ако искате да имате инвентарен файл за няколко хоста, но да работите само за една група, това може да стане със следната команда:
ansible-playbook -i inventory ./playbook.yml -l "staging"
Друга възможност е да имате отделни файлове за инвентаризация за различни групи. Или можете да комбинирате двата подхода, ако имате много различни хостове.
Нека се върнем към настройката на 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
Тук всичко е просто - отново анзибилни модули с доста стандартен синтаксис. Но има един момент. Няма смисъл да рестартирате nginx всеки път. Забелязали ли сте, че ние не пишем команди като: „направи това така“, синтаксисът изглежда по-скоро като „това трябва да има това състояние“. И най-често анзибълът работи точно така. Ако групата вече съществува или системният пакет вече е инсталиран, тогава ansible ще провери за това и ще пропусне задачата. Освен това файловете няма да бъдат копирани, ако напълно съответстват на това, което вече е на сървъра. Можем да се възползваме от това и да рестартираме nginx само ако конфигурационните файлове са променени. За това има директива за регистър:
# 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 към основната игра.
Настройка на 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 }}"
Няма да описвам как се добавят променливи към инвентара, това вече е правено много пъти, както и синтаксиса на модулите postgresql_db и postgresql_user. Повече информация можете да намерите в документацията. Най-интересната директива тук е become_user: postgres
. Факт е, че по подразбиране само потребителят на postgres има достъп до базата данни postgresql и то само локално. Тази директива ни позволява да изпълняваме команди от името на този потребител (ако имаме достъп, разбира се).
Освен това може да се наложи да добавите ред към 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 е инсталиран в началната си директория, а не глобално. И също така използваме 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 ще трябва да се дефинира директно в същия скрипт.
Следващият проблем се дължи на факта, че командата на обвивката няма състояние от анзибилна гледна точка. Тоест няма да има автоматична проверка дали тази версия на 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
(тук можете да създавате различни файлове за различни групи хостове, точно както във файла с инвентара: production.yml, staging.yml и т.н.).
Всички променливи, които трябва да бъдат криптирани, трябва да бъдат прехвърлени в този файл, като се използва стандартен 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
файлът може да бъде дешифриран, модифициран и след това отново криптиран.
Не е необходимо да дешифрирате файла, за да работи. Съхранявате го шифрован и стартирате книгата с аргумента --ask-vault-pass
. Ansible ще поиска паролата, ще извлече променливите и ще изпълни задачите. Всички данни ще останат криптирани.
Пълната команда за няколко групи хостове и ansible vault ще изглежда по следния начин:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
Но няма да ви дам пълния текст на книгите и ролите, напишете го сами. Тъй като ansible е така - ако не разбирате какво трябва да се направи, тогава той няма да го направи вместо вас.
Източник: www.habr.com