Configurar un servidor para implementar unha aplicación Rails usando Ansible

Non hai moito que necesitaba escribir varios libros de xogos de Ansible para preparar o servidor para a implantación dunha aplicación Rails. E, sorprendentemente, non atopei un sinxelo manual paso a paso. Non quería copiar o libro de xogos doutro sen entender o que pasaba e, ao final, tiven que ler a documentación, recollendo todo eu. Quizais poida axudar a alguén a acelerar este proceso coa axuda deste artigo.

O primeiro que hai que entender é que ansible ofrécelle unha interface conveniente para realizar unha lista predefinida de accións nun servidor remoto a través de SSH. Non hai maxia aquí, non podes instalar un complemento e obter unha implementación sen tempo de inactividade da túa aplicación con docker, monitorización e outros beneficios fóra da caixa. Para escribir un libro de xogos, debes saber exactamente o que queres facer e como facelo. É por iso que non estou satisfeito cos libros de xogos preparados de GitHub ou con artigos como: "Copia e executa, funcionará".

Que necesitamos?

Como xa dixen, para escribir un playbook cómpre saber o que queres facer e como facelo. Imos decidir o que necesitamos. Para unha aplicación Rails necesitaremos varios paquetes de sistema: nginx, postgresql (redis, etc). Ademais, necesitamos unha versión específica de ruby. O mellor é instalalo a través de rbenv (rvm, asdf...). Executar todo isto como usuario root sempre é unha mala idea, polo que cómpre crear un usuario separado e configurar os seus dereitos. Despois diso, cómpre cargar o noso código ao servidor, copiar as configuracións de nginx, postgres, etc. e iniciar todos estes servizos.

Como resultado, a secuencia de accións é a seguinte:

  1. Inicie sesión como root
  2. instalar paquetes do sistema
  3. crear un novo usuario, configurar dereitos, clave ssh
  4. configurar paquetes do sistema (nginx, etc.) e executalos
  5. Creamos un usuario na base de datos (podes crear inmediatamente unha base de datos)
  6. Inicia sesión como usuario novo
  7. Instala rbenv e ruby
  8. Instalación do bundler
  9. Cargando o código da aplicación
  10. Iniciando o servidor Puma

Ademais, as últimas etapas pódense facer usando capistrano, polo menos fóra da caixa pode copiar o código en directorios de versións, cambiar a versión cunha ligazón simbólica tras a implementación exitosa, copiar configuracións dun directorio compartido, reiniciar puma, etc. Todo isto pódese facer usando Ansible, pero por que?

Estrutura do ficheiro

Ansible ten estrito estrutura do ficheiro para todos os teus ficheiros, polo que é mellor gardalos todos nun directorio separado. Ademais, non é tan importante se estará na propia aplicación de carrís ou por separado. Podes almacenar ficheiros nun repositorio git separado. Persoalmente, pareceume máis conveniente crear un directorio ansible no directorio /config da aplicación rails e almacenar todo nun repositorio.

Libro de xogos sinxelo

Playbook é un ficheiro yml que, usando unha sintaxe especial, describe o que debe facer Ansible e como. Imos crear o primeiro playbook que non fai nada:

---
- name: Simple playbook
  hosts: all

Aquí simplemente dicimos que o noso playbook se chama Simple Playbook e que o seu contido debería executarse para todos os hosts. Podemos gardalo no directorio /ansible co nome playbook.yml e tenta correr:

ansible-playbook ./playbook.yml

PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched

Ansible di que non coñece ningún host que coincida coa lista de todos. Deben figurar nun especial arquivo de inventario.

Creémolo no mesmo directorio ansible:

123.123.123.123

Así é como simplemente especificamos o host (idealmente o host do noso VPS para probar, ou pode rexistrar localhost) e gárdao baixo o nome inventory.
Podes probar a executar ansible cun ficheiro de inventario:

ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************************

PLAY RECAP ************************************************************************************************************************************

Se tes acceso ssh ao host especificado, ansible conectarase e recollerá información sobre o sistema remoto. (TAREFA predeterminada [Recopilación de feitos]) despois da cal dará un breve informe sobre a execución (PLAY RECAP).

De forma predeterminada, a conexión usa o nome de usuario co que iniciaches sesión no sistema. O máis probable é que non estea no host. No ficheiro do playbook, pode especificar que usuario usar para conectarse mediante a directiva remote_user. Ademais, a información sobre un sistema remoto pode ser moitas veces innecesaria para ti e non debes perder o tempo recollela. Esta tarefa tamén se pode desactivar:

---
- name: Simple playbook
  hosts: all
  remote_user: root
  become: true
  gather_facts: no

Tenta executar o playbook de novo e asegúrate de que a conexión funciona. (Se especificaches o usuario root, tamén debes especificar a directiva converte: true para obter dereitos elevados. Como está escrito na documentación: become set to ‘true’/’yes’ to activate privilege escalation. aínda que non está do todo claro por que).

Quizais reciba un erro causado polo feito de que ansible non pode determinar o intérprete de Python, entón pode especificalo manualmente:

ansible_python_interpreter: /usr/bin/python3 

Podes descubrir onde tes python co comando whereis python.

Instalación de paquetes do sistema

A distribución estándar de Ansible inclúe moitos módulos para traballar con varios paquetes do sistema, polo que non temos que escribir scripts bash por ningún motivo. Agora necesitamos un destes módulos para actualizar o sistema e instalar os paquetes do sistema. Teño Ubuntu Linux no meu VPS, así que para instalar paquetes que uso apt-get и módulo para iso. Se estás a usar un sistema operativo diferente, quizais necesites un módulo diferente (lembra que dixen ao principio que necesitamos saber de antemán que e como imos facer). Non obstante, a sintaxe é moi probable que sexa semellante.

Complementemos o noso manual coas primeiras tarefas:

---
- 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

A tarefa é exactamente a tarefa que Ansible realizará nos servidores remotos. Dámoslle un nome á tarefa para que poidamos seguir a súa execución no rexistro. E describimos, utilizando a sintaxe dun módulo específico, o que ten que facer. Neste caso apt: update_cache=yes - di que actualice os paquetes do sistema usando o módulo apt. O segundo comando é un pouco máis complicado. Pasamos unha lista de paquetes ao módulo apt e dicimos que o son state debería converterse present, é dicir, dicimos instalar estes paquetes. Do mesmo xeito, podemos indicarlles que os eliminen ou que os actualicen simplemente cambiando state. Teña en conta que para que os rails funcionen con postgresql necesitamos o paquete postgresql-contrib, que estamos instalando agora. De novo, debes saber e facelo; ansible por si só non o fará.

Tenta executar o playbook de novo e comprobe que os paquetes están instalados.

Creación de novos usuarios.

Para traballar cos usuarios, Ansible tamén ten un módulo - usuario. Engadimos unha tarefa máis (ocultei as partes xa coñecidas do libro de xogos detrás dos comentarios para non copialo por completo cada vez):

---
- name: Simple playbook
  # ...
  tasks:
    # ...
    - name: Add a new user
      user:
        name: my_user
        shell: /bin/bash
        password: "{{ 123qweasd | password_hash('sha512') }}"

Creamos un novo usuario, establecemos un schell e un contrasinal para el. E entón atopamos varios problemas. E se os nomes de usuario teñen que ser diferentes para diferentes hosts? E almacenar o contrasinal en texto claro no manual é unha moi mala idea. Para comezar, poñamos o nome de usuario e o contrasinal en variables, e cara ao final do artigo mostrarei como cifrar o contrasinal.

---
- name: Simple playbook
  # ...
  tasks:
    # ...
    - name: Add a new user
      user:
        name: "{{ user }}"
        shell: /bin/bash
        password: "{{ user_password | password_hash('sha512') }}"

As variables establécense nos libros de xogos usando chaves dobres.

Indicaremos os valores das variables no ficheiro de inventario:

123.123.123.123

[all:vars]
user=my_user
user_password=123qweasd

Teña en conta a directiva [all:vars] - di que o seguinte bloque de texto son variables (vars) e son aplicables a todos os hosts (todos).

O deseño tamén é interesante "{{ user_password | password_hash('sha512') }}". O caso é que ansible non instala o usuario vía user_add como o farías manualmente. E garda todos os datos directamente, polo que tamén debemos converter o contrasinal nun hash previamente, que é o que fai este comando.

Engadimos o noso usuario ao grupo sudo. Non obstante, antes debemos asegurarnos de que existe tal grupo porque ninguén o fará por nós:

---
- 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"

Todo é bastante sinxelo, tamén temos un módulo de grupos para a creación de grupos, cunha sintaxe moi parecida á de apt. Entón é suficiente con rexistrar este grupo para o usuario (groups: "sudo").
Tamén é útil engadir unha clave ssh a este usuario para que poidamos iniciar sesión usando ela sen contrasinal:

---
- 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

Neste caso, o deseño é interesante "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — copia o contido do ficheiro id_rsa.pub (o teu nome pode ser diferente), é dicir, a parte pública da clave ssh e súbea á lista de claves autorizadas para o usuario no servidor.

Papeis

As tres tarefas para crear uso pódense clasificar facilmente nun grupo de tarefas, e sería unha boa idea almacenar este grupo por separado do manual principal para que non medre demasiado. Para este fin, Ansible ten papeis.
Segundo a estrutura de ficheiros indicada ao principio, os roles deben colocarse nun directorio de roles separado, para cada rol hai un directorio separado co mesmo nome, dentro do directorio de tarefas, ficheiros, modelos, etc.
Imos crear unha estrutura de ficheiros: ./ansible/roles/user/tasks/main.yml (main é o ficheiro principal que se cargará e executará cando se conecte un rol ao playbook; pódense conectar a el outros ficheiros de rol). Agora podes transferir todas as tarefas relacionadas co usuario a este ficheiro:

# 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

No manual principal, debes especificar para usar o rol de usuario:

---
- 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

Ademais, pode ter sentido actualizar o sistema antes de todas as outras tarefas; para facelo, pode renomear o bloque tasks no que se definen en pre_tasks.

Configurando nginx

Xa deberíamos ter instalado Nginx, necesitamos configuralo e executalo. Imos facelo de inmediato no papel. Imos crear unha estrutura de ficheiros:

- ansible
  - roles
    - nginx
      - files
      - tasks
        - main.yml
      - templates

Agora necesitamos ficheiros e modelos. A diferenza entre eles é que ansible copia os ficheiros directamente, tal e como está. E os modelos deben ter a extensión j2 e poden usar valores variables usando as mesmas chaves dobres.

Imos habilitar nginx main.yml arquivo. Para iso temos un módulo systemd:

# Copy nginx configs and start it
- name: enable service nginx and start
  systemd:
    name: nginx
    state: started
    enabled: yes

Aquí non só dicimos que nginx debe ser iniciado (é dicir, lanzámolo), senón que inmediatamente dicimos que debe estar activado.
Agora imos copiar os ficheiros de configuración:

# 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'

Creamos o ficheiro de configuración principal de nginx (pode levalo directamente do servidor ou escribilo vostede mesmo). E tamén o ficheiro de configuración da nosa aplicación no directorio sites_available (isto non é necesario pero útil). No primeiro caso, usamos o módulo de copia para copiar ficheiros (o ficheiro debe estar en /ansible/roles/nginx/files/nginx.conf). No segundo, copiamos o modelo, substituíndo os valores das variables. O modelo debe estar dentro /ansible/roles/nginx/templates/my_app.j2). E pode parecer algo así:

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 }};
  ....
}

Preste atención ás insercións {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — Estas son todas as variables cuxos valores Ansible substituirá no modelo antes de copialo. Isto é útil se usas un manual de xogos para diferentes grupos de anfitrións. Por exemplo, podemos engadir o noso ficheiro de inventario:

[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

Se agora lanzamos o noso playbook, realizará as tarefas especificadas para ambos hosts. Pero, ao mesmo tempo, para un host de posta en escena, as variables serán diferentes ás de produción, e non só nos papeis e nos playbooks, senón tamén nas configuracións de nginx. {{ inventory_hostname }} non precisa especificarse no ficheiro de inventario - isto variable ansible especial e alí gárdase o host para o que se está a executar o playbook.
Se queres ter un ficheiro de inventario para varios hosts, pero só executalo para un grupo, pódese facer co seguinte comando:

ansible-playbook -i inventory ./playbook.yml -l "staging"

Outra opción é ter ficheiros de inventario separados para diferentes grupos. Ou podes combinar os dous enfoques se tes moitos hosts diferentes.

Volvamos á configuración de nginx. Despois de copiar os ficheiros de configuración, necesitamos crear unha ligazón simbólica en sitetest_enabled para my_app.conf desde sites_available. E reinicia 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

Aquí todo é sinxelo: de novo módulos ansibles cunha sintaxe bastante estándar. Pero hai un punto. Non ten sentido reiniciar nginx cada vez. Observaches que non escribimos comandos como: "faga isto así", a sintaxe parece máis "isto debería ter este estado". E a maioría das veces así é exactamente como funciona ansible. Se o grupo xa existe ou o paquete do sistema xa está instalado, entón ansible comprobará isto e saltará a tarefa. Ademais, os ficheiros non se copiarán se coinciden completamente co que xa está no servidor. Podemos aproveitar isto e reiniciar nginx só se se cambiaron os ficheiros de configuración. Hai unha directiva de rexistro para isto:

# 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

Se un dos ficheiros de configuración cambia, farase unha copia e rexistrarase a variable restart_nginx. E só se esta variable foi rexistrada, o servizo reiniciarase.

E, por suposto, cómpre engadir o papel nginx ao manual principal.

Configurando postgresql

Necesitamos habilitar postgresql usando systemd do mesmo xeito que fixemos con nginx, e tamén crear un usuario que usaremos para acceder á base de datos e á propia base de datos.
Imos crear un papel /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 }}"

Non vou describir como engadir variables ao inventario, isto xa se fixo moitas veces, así como a sintaxe dos módulos postgresql_db e postgresql_user. Pódese atopar máis información na documentación. A directiva máis interesante aquí é become_user: postgres. O caso é que, por defecto, só o usuario de postgres ten acceso á base de datos postgresql e só localmente. Esta directiva permítenos executar comandos en nome deste usuario (se temos acceso, claro).
Ademais, pode ter que engadir unha liña a pg_hba.conf para permitir o acceso dun novo usuario á base de datos. Isto pódese facer do mesmo xeito que cambiamos a configuración de nginx.

E, por suposto, cómpre engadir o rol postgresql ao manual principal.

Instalación de ruby ​​via rbenv

Ansible non ten módulos para traballar con rbenv, pero instálase clonando un repositorio git. Polo tanto, este problema convértese no máis non estándar. Imos crear un papel para ela /ansible/roles/ruby_rbenv/main.yml e comezamos a enchelo:

# Install rbenv and ruby
- name: Install rbenv
  become_user: "{{ user }}"
  git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv

Usamos de novo a directiva become_user para traballar baixo o usuario que creamos para estes fins. Xa que rbenv está instalado no seu directorio persoal, e non globalmente. E tamén usamos o módulo git para clonar o repositorio, especificando repo e dest.

A continuación, necesitamos rexistrar rbenv init en bashrc e engadir alí rbenv a PATH. Para iso temos o módulo 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 -)"'

Despois necesitas instalar ruby_build:

- name: Install ruby-build
  become_user: "{{ user }}"
  git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build

E finalmente instala Ruby. Isto faise a través de rbenv, é dicir, simplemente co comando 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

Dicimos que comando executar e con que. Non obstante, aquí atopamos o feito de que ansible non executa o código contido en bashrc antes de executar os comandos. Isto significa que rbenv terá que definirse directamente no mesmo script.

O seguinte problema débese ao feito de que o comando shell non ten ningún estado desde un punto de vista ansible. É dicir, non haberá unha verificación automática de se esta versión de Ruby está instalada ou non. Podemos facelo nós mesmos:

- 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

Todo o que queda é instalar bundler:

- name: Install bundler
  become_user: "{{ user }}"
  shell: |
    export PATH="${HOME}/.rbenv/bin:${PATH}"
    eval "$(rbenv init -)"
    gem install bundler

E de novo, engade o noso papel ruby_rbenv ao manual principal.

Ficheiros compartidos.

En xeral, a configuración pódese completar aquí. A continuación, só queda executar capistrano e copiará o propio código, creará os directorios necesarios e lanzará a aplicación (se todo está configurado correctamente). Non obstante, capistrano a miúdo require ficheiros de configuración adicionais, como database.yml ou .env Pódense copiar como ficheiros e modelos para nginx. Só hai unha sutileza. Antes de copiar ficheiros, cómpre crear unha estrutura de directorios para eles, algo así:

# Copy shared files for deploy
- name: Ensure shared dir
  become_user: "{{ user }}"
  file:
    path: "{{ app_path }}/shared/config"
    state: directory

especificamos só un directorio e ansible creará automaticamente os pai se é necesario.

Ansible Vault

Xa nos atopamos co feito de que as variables poden conter datos secretos como o contrasinal do usuario. Se creou .env arquivo para a solicitude, e database.yml entón debe haber aínda máis datos tan críticos. Sería bo ocultalos das miradas indiscretas. Para este fin úsase bóveda ansible.

Imos crear un ficheiro para as variables /ansible/vars/all.yml (aquí podes crear ficheiros diferentes para diferentes grupos de hosts, igual que no ficheiro de inventario: production.yml, staging.yml, etc.).
Todas as variables que se deben cifrar deben transferirse a este ficheiro mediante a sintaxe estándar 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

Despois de que este ficheiro pódese cifrar co comando:

ansible-vault encrypt ./vars/all.yml

Por suposto, ao cifrar, terás que establecer un contrasinal para o descifrado. Podes ver o que haberá dentro do ficheiro despois de chamar a este comando.

Por medio de ansible-vault decrypt o ficheiro pode ser descifrado, modificado e despois cifrado de novo.

Non é necesario descifrar o ficheiro para funcionar. Almacénao cifrado e executa o playbook co argumento --ask-vault-pass. Ansible pedirá o contrasinal, recuperará as variables e executará as tarefas. Todos os datos permanecerán encriptados.

O comando completo para varios grupos de hosts e bóveda ansible terá un aspecto así:

ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass

Pero non che darei o texto completo dos libros de teatro e dos papeis, escríbeo ti mesmo. Porque ansible é así: se non entendes o que hai que facer, non o fará por ti.

Fonte: www.habr.com

Engadir un comentario