Неотдавна ми се наложи да напиша няколко ansible playbooks за подготовка на сървъра за деплой на rails приложение. И, за моя изненада, не намерих простичко ръководство стъпка по стъпка. Не исках да копирам чужд плейбук без да разбирам какво се случва, затова в крайна сметка ми се наложи да прочета документацията, събирайки всичко сам. Може би с тази статия ще мога да помогна на някого да ускори този процес.
Първо е важно да разберете, че ansible ви предоставя удобен интерфейс за изпълнение на предварително определен списък от действия на отдалечен сървър (сървъри) чрез SSH. Тук няма никаква магия; не можете да инсталирате плъгин и да получите от кутията zero downtime деплой на вашето приложение с docker, мониторинг и други екстри. За да напишете плейбук, трябва да знаете точно какво искате да направите и как да го направите. Затова не ми харесват готовите плейбуци от GitHub или статии от типа: „Копирайте и стартирайте - ще работи”.
Какво ни трябва?
Както вече казах, за да напишете плейбук, трябва да знаете какво искате да направите и как да го направите. Нека да определим какво точно ни трябва. За Rails приложение, ще са ни необходими няколко системни пакета: nginx, postgresql (redis и т.н.). Освен това, ще ни трябва ruby в определена версия. Най-добре е да го инсталираме чрез rbenv (rvm, asdf…). Да стартираме всичко това под root потребител — винаги е лоша идея, затова трябва да създадем отделен потребител и да настроим правата му. След това е необходимо да качим кода на сървъра, да копираме конфигурациите за nginx, postgres и т.н. и да стартираме всички тези услуги.
В крайна сметка, последователността на действията е следната:
- Логваме се като root
- инсталираме системните пакети
- създаваме нов потребител, настройваме правата и SSH ключа
- настройваме системните пакети (nginx и т.н.) и ги стартираме
- Създаваме потребител в БД (можем да създадем и базата веднага)
- Логваме се с новия потребител
- Инсталираме rbenv и ruby
- Инсталираме bundler
- Качваме кода на приложението
- Стартираме Puma сървъра
Последните етапи могат да се извършват с помощта на capistrano, понеже тя от кутията може да копира кода в директориите за версии, да премества версията с симлинк при успешен деплой, да копира конфигурации от споделена директория, да рестартира puma и т.н. Всичко това може да се направи и с Ansible, но защо?
Файловата структура
Ansible има строга за всички свои файлове, затова е най-добре да ги държите в отделна директория. Не е толкова важно дали ще бъде в самото rails приложение или отделно. Може да съхранявате файлове в отделен git репозиторий. На мен ми беше най-удобно да създам директория ansible в /config директорията на rails приложението и да съхранявам всичко в един репозиторий.
Simple Playbook
Playbook — това е yml файл, в който с помощта на специален синтаксис е описано какво и как ansible трябва да направи. Нека да създадем първия плейбук, който не прави нищо:
---
- name: Simple playbook
hosts: allТук просто казваме, че нашият плейбук се нарича Simple Playbook и че съдържанието му трябва да се изпълнява за всички хостове. Можем да го запишем в директорията /ansible под името playbook.yml и да опитаме да го стартираме:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matchedAnsible казва, че не знае хостовете, които съответстват на списъка all. Те трябва да бъдат изброени в специален .
Нека да го създадем в същата ansible директория:
123.123.123.123Ето как просто указваме хост (в идеалния случай хост на вашия VPS за тестове, или просто можете да запишете localhost) и го запазваме под името inventory.
Можете да опитате да стартирате ansible с inventory файла:
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 е зададено на 'true'/'yes', за да активирате повишаване на правата. въпреки че не е съвсем ясно защо).
Възможно е да получите грешка, предизвикана от това, че ansible не може да определи интерпретатора на Python, в такъв случай можете да го укажете ръчно:
ansible_python_interpreter: /usr/bin/python3 къде имате инсталиран python можете да разберете с командата whereis python.
Инсталиране на системни пакети
В стандартната доставка на Ansible се съдържат множество модули за работа с различни системни пакети, благодарение на което не е необходимо да пишем bash скриптове за всяка дреболия. В момента ни трябва един от тези модули за обновление на системата и инсталиране на системни пакети. На моя VPS е инсталиран Ubuntu Linux, съответно за инсталиране на пакети използвам apt-get и . Ако използвате друга операционна система, може да е необходим друг модул (помнете, че в началото споменах, че трябва предварително да знаем какво и как ще правим). Въпреки това, синтаксисът вероятно ще бъде подобен.
Нека допълним нашия playbook с първите задачи:
---
- name: Прост playbook
hosts: all
remote_user: root
become: true
gather_facts: no
tasks:
- name: Обновяване на системата
apt: update_cache=yes
- name: Инсталиране на системните зависимости
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: presentTask — това е задачата, която ansible ще изпълнява на отдалечените сървъри. Даваме име на задачата, за да можем да проследим нейното изпълнение в логовете. И описваме, с помощта на синтаксиса на конкретния модул, какво трябва да направи. В случая apt: update_cache=yes — казва да обнови пакетите на системата с помощта на модула apt. Втората команда е малко по-сложна. Предаваме на модула apt списък с пакети и казваме, че те state трябва да станат представен, тоест казваме да инсталира тези пакети. По подобен начин можем да кажем да ги премахнем или обновим, просто сменяйки state. Обърнете внимание, че за работата на rails с postgresql ни трябва пакетът postgresql-contrib, който в момента инсталираме. Отново, трябва да знаем и направим това, ansible няма да го направи сам.
Опитайте да стартирате playbook отново и проверете, че пакетите ще се инсталират.
Създаване на нови потребители.
За работа с потребителите в Ansible също има модул — user. Нека добавим още една задача (скрих известните части от плейбука за целите на коментара, за да не го копираме цяло всеки път):
---
- name: Прост плейбук
# ...
tasks:
# ...
- name: Добавяне на нов потребител
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"Създаваме нов потребител, задаваме му shell и парола. И тук се сблъскваме с няколко проблема. Какво, ако имената на потребителите трябва да са различни за различни хостове? И да съхраняваме паролата в непредставен вид в плейбука е много лоша идея. Първо, нека изнесем името на потребителя и паролата в променливи, а по-близо до края на статията ще покажа как да се шифрова паролата.
---
- name: Прост плейбук
# ...
tasks:
# ...
- name: Добавяне на нов потребител
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: Прост плейбук
# ...
tasks:
# ...
- name: Уверете се, че групата 'sudo' съществува
group:
name: sudo
state: present
- name: Добавяне на нов потребител
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"Всичко е достатъчно просто, имаме и модул group за създаване на групи, със синтаксис, много подобен на apt. След което просто трябва да зададем тази група на потребителя (groups: "sudo").
Също така е полезно да добавим този потребител ssh ключ, за да можем да влизаме в него без парола:
---
- name: Проста плейбука
# ...
tasks:
# ...
- name: Осигурете група 'sudo'
group:
name: sudo
state: present
- name: Добавете нов потребител
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Разположете SSH ключ
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 и т.н.
Създаваме файлова структура: ./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: Проста плейбука
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Актуализиране на системата
apt: update_cache=yes
- name: Инсталиране на системни зависимости
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 трябва да бъде стартиран, но веднага уточняваме, че той трябва да бъде активиран.
Сега ще копираме конфигурационните файлове:
# 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. След копирането на конфигурационните файлове, е необходимо да създадем симлинк в sites-enabled към my_app.conf от sites_available. И да рестартираме nginx.
... # стар код в 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 в основния playbook.
Инсталиране на 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: Добавяне на rbenv в PATH
become_user: "{{ user }}"
lineinfile:
path: ~\/\.bashrc
state: present
line: 'export PATH="${HOME}\/\.rbenv\/bin:${PATH}"'
- name: Добавяне на rbenv init в bashrc
become_user: "{{ user }}"
lineinfile:
path: ~\/\.bashrc
state: present
line: 'eval "$(rbenv init -)"'След това е нужно да инсталираме ruby_build:
- name: Инсталиране на ruby-build
become_user: "{{ user }}"
git: repo=https:\/\/github.com\/rbenv\/ruby-build.git dest=~\/\.rbenv\/plugins\/ruby-buildИ накрая, инсталираме ruby. Това става чрез rbenv, просто с bash команда:
- name: Инсталиране на 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: Инсталиране на 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: Инсталиране на 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 и т.н.).
В този файл трябва да пренесете всички променливи, които трябва да бъдат криптирани, използвайки стандартния 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А пълният текст на playbooks и роли няма да ви дам, пишете сами. Защото ansible е такова нещо - ако не разбирате какво трябва да направите, той няма да го направи вместо вас.
Източник: habr.com
