Конфигуриране на сървър за разполагане на Rails приложение с помощта на Ansible

Неотдавна трябваше да напиша няколко Ansible playbooks, за да подготвя сървъра за внедряване на Rails приложение. И, изненадващо, не намерих просто ръководство стъпка по стъпка. Не исках да копирам нечия друга книга, без да разбирам какво се случва, и в крайна сметка трябваше да прочета документацията, събирайки всичко сам. Може би мога да помогна на някого да ускори този процес с помощта на тази статия.

Първото нещо, което трябва да разберете е, че ansible ви предоставя удобен интерфейс за извършване на предварително дефиниран списък от действия на отдалечен сървър(и) чрез SSH. Тук няма магия, не можете да инсталирате плъгин и да получите внедряване на вашето приложение без прекъсване с докер, мониторинг и други екстри. За да напишете книга-игра, трябва да знаете какво точно искате да направите и как да го направите. Ето защо не съм доволен от готови книги за игри от GitHub или статии като: „Копирай и стартирай, ще работи.“

Какво ни трябва?

Както вече казах, за да напишете книга-игра трябва да знаете какво искате да направите и как да го направите. Да решим какво ни трябва. За Rails приложение ще ни трябват няколко системни пакета: nginx, postgresql (redis и т.н.). Освен това се нуждаем от специфична версия на ruby. Най-добре е да го инсталирате чрез rbenv (rvm, asdf...). Изпълнението на всичко това като root потребител винаги е лоша идея, така че трябва да създадете отделен потребител и да конфигурирате неговите права. След това трябва да качите нашия код на сървъра, да копирате конфигурациите за nginx, postgres и т.н. и да стартирате всички тези услуги.

В резултат на това последователността от действия е следната:

  1. Влезте като root
  2. инсталирайте системни пакети
  3. създайте нов потребител, конфигурирайте права, ssh ключ
  4. конфигурирайте системни пакети (nginx и т.н.) и ги стартирайте
  5. Създаваме потребител в базата данни (можете веднага да създадете база данни)
  6. Влезте като нов потребител
  7. Инсталирайте rbenv и ruby
  8. Инсталиране на пакета
  9. Качване на кода на приложението
  10. Стартиране на Puma сървър

Освен това, последните етапи могат да бъдат направени с помощта на capistrano, поне извън кутията той може да копира код в директории за издаване, да превключва изданието със символна връзка при успешно разгръщане, да копира конфигурации от споделена директория, да рестартира puma и т.н. Всичко това може да се направи с помощта на Ansible, но защо?

Файлова структура

Ansible има строг файлова структура за всички ваши файлове, така че е най-добре да ги съхранявате в отделна директория. Освен това не е толкова важно дали ще бъде в самото приложение на релсите или отделно. Можете да съхранявате файлове в отделно git хранилище. Лично аз намерих за най-удобно да създам ansible директория в /config директорията на rails приложението и да съхранявам всичко в едно хранилище.

Проста книга за игра

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

Добавяне на нов коментар