Configurar un servidor para desplegar una aplicación Rails usando Ansible

No hace mucho tiempo, necesitaba escribir varios libros de jugadas ansibles para preparar el servidor para implementar la aplicación de rieles. Y, sorprendentemente, no encontré un manual simple paso a paso. No quería copiar el libro de jugadas de otra persona sin comprender lo que estaba sucediendo y, como resultado, tuve que leer la documentación y recopilar todo yo mismo. Quizás alguien a quien pueda ayudar a acelerar este proceso con este artículo.


Lo primero que debe entender es que ansible le proporciona una interfaz conveniente para realizar una lista predefinida de acciones en los servidores remotos a través de SSH. Aquí no hay magia, no puede instalar el complemento y salir de la caja de tiempo de inactividad cero la implementación de su aplicación con docker, monitoreo y otras cosas. Para escribir un libro de jugadas, debe saber qué es exactamente lo que quiere hacer y cómo hacerlo. Por lo tanto, no me gustan los libros de jugadas listos para usar del github, o artículos como: "Copiar y ejecutar, funcionará".


Que necesitamos


Como dije, para escribir un libro de jugadas necesitas saber qué quieres hacer y cómo hacerlo. Decidamos lo que necesitamos. Para una aplicación Rails, necesitaremos varios paquetes del sistema: nginx, postgresql (redis, etc.). Además, necesitamos un rubí de cierta versión. Lo mejor es instalarlo a través de rbenv (rvm, asdf ...). Ejecutar todo esto desde la raíz del usuario siempre es una mala idea, por lo que debe crear un usuario separado y configurar los derechos para él. Después de eso, debe cargar nuestro código en el servidor, copiar las configuraciones para nginx, postgres, etc. e iniciar todos estos servicios.


Como resultado, la secuencia de acciones es la siguiente:


  1. Inicie sesión como root
  2. instalar paquetes del sistema
  3. crear un nuevo usuario, configurar derechos, clave ssh
  4. configurar paquetes del sistema (nginx, etc.) y ejecutarlos
  5. Cree un usuario en la base de datos (puede crear inmediatamente una base de datos)
  6. Inicie sesión como nuevo usuario
  7. Instalar rbenv y ruby
  8. Instalar el paquete
  9. Rellene el código de solicitud
  10. Iniciamos el servidor Puma

Además, los últimos pasos se pueden hacer usando capistrano, al menos ella puede copiar el código en los directorios de lanzamiento desde el cuadro, cambiar el lanzamiento con un enlace simbólico en una implementación exitosa, copiar configuraciones desde el directorio compartido, reiniciar puma, etc. Todo esto se puede hacer con Ansible, pero ¿por qué?


Estructura de archivo


Ansible tiene una estructura de archivos estricta para todos sus archivos, por lo que es mejor mantenerlo todo en un directorio separado. Y no es tan importante si estará en la aplicación de rieles en sí o por separado. Puede almacenar archivos en un repositorio git separado. Personalmente, me pareció más conveniente crear el directorio ansible en el directorio / config de la aplicación rails y almacenar todo en un repositorio.


Libro de jugadas simple


Playbook es un archivo yml que describe qué y cómo debe hacer ansible usando una sintaxis especial. Creemos el primer libro de jugadas que no hace nada:


--- - name: Simple playbook hosts: all 

Aquí, simplemente decimos que nuestro libro de jugadas se llama Simple Playbook y que su contenido debería ejecutarse para todos los hosts. Podemos guardarlo en el directorio / ansible con el nombre playbook.yml e intentar ejecutar:


 ansible-playbook ./playbook.yml PLAY [Simple Playbook] ************************************************************************************************************************************ skipping: no hosts matched 

Ansible dice que no conoce hosts que coincidan con la lista de todos. Deben figurar en un archivo de inventario especial.


Vamos a crearlo en el mismo directorio ansible:


 123.123.123.123 

Así que solo especifique el host (idealmente, el host de su VPS para las pruebas, o puede registrar localhost) y guárdelo bajo el nombre de inventory .
Puede intentar ejecutar ansible con un archivo invetory:


 ansible-playbook ./playbook.yml -i inventory PLAY [Simple Playbook] ************************************************************************************************************************************ TASK [Gathering Facts] ************************************************************************************************************************************ PLAY RECAP ************************************************************************************************************************************ 

Si tiene acceso ssh al host especificado, ansible se conectará y recopilará información sobre el sistema remoto. (TAREA predeterminada [Hechos de recopilación]) después de lo cual dará un breve informe de progreso (REPRODUCCIÓN DE REPRODUCCIÓN).


Por defecto, el nombre de usuario con el que ha iniciado sesión en el sistema se utiliza para la conexión. Lo más probable es que no esté en el host. En el archivo de libro de jugadas, puede especificar qué usuario usar para conectarse usando la directiva remote_user. Además, la información sobre un sistema remoto a menudo puede ser innecesaria para usted y no debe perder el tiempo reuniéndola. También puedes desactivar esta tarea:


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

Intente ejecutar el libro de jugadas nuevamente y asegúrese de que la conexión esté funcionando. (Si especificó la raíz del usuario, también debe especificar la directiva Become: true para obtener derechos elevados. Como dice la documentación: become set to 'true'/'yes' to activate privilege escalation. Aunque no está claro por qué) .


Quizás reciba un error causado por el ansible que el intérprete de Python no puede determinar, luego puede especificarlo manualmente:


 ansible_python_interpreter: /usr/bin/python3 

donde tiene python se puede encontrar con el comando whereis python .


Instalar paquetes del sistema


Ansible viene con muchos módulos para trabajar con varios paquetes del sistema, por lo que no tenemos que escribir scripts de bash por ningún motivo. Ahora necesitamos uno de estos módulos para actualizar el sistema e instalar paquetes del sistema. Tengo Ubuntu Linux en VPS, respectivamente, para instalar paquetes, uso apt-get y un módulo para ello . Si utiliza un sistema operativo diferente, es posible que necesite un módulo diferente (recuerde, dije al principio que necesitamos saber de antemano qué y cómo lo haremos). Sin embargo, es probable que la sintaxis sea similar.


Complementamos nuestro libro de jugadas con las primeras tareas:


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

La tarea es solo la tarea que ansible realizará en servidores remotos. Le damos un nombre a la tarea para seguir su progreso en el registro. Y describimos, utilizando la sintaxis de un módulo específico, lo que debe hacer. En este caso, apt: update_cache=yes - dice que actualice los paquetes del sistema usando el módulo apt. El segundo equipo es un poco más complicado. Pasamos la lista de paquetes al módulo apt, y decimos que su state debe estar present , es decir, decimos instalar estos paquetes. Del mismo modo, podemos decirles que se eliminen o actualicen simplemente cambiando el state . Tenga en cuenta que para que los rieles funcionen con postgresql, necesitamos el paquete postgresql-contrib que estamos instalando actualmente. Esto nuevamente necesita ser conocido y hecho, ansible por sí mismo no lo hará.


Intente ejecutar el libro de jugadas nuevamente y verifique que los paquetes estén instalados.


Creación de nuevos usuarios.


Para trabajar con usuarios, Ansible también tiene un módulo: usuario. Agregue otra tarea (escondí las partes ya conocidas del libro de jugadas detrás de los comentarios, para no copiarlo 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 nuevo usuario, le configuramos un esquema y una contraseña. Y luego nos enfrentamos a varios problemas. ¿Qué pasa si los nombres de usuario deben ser diferentes para diferentes hosts? Sí, y mantener la contraseña abierta en el libro de jugadas es una muy mala idea. Para comenzar, colocaremos el nombre de usuario y la contraseña en variables, y hacia el final del artículo mostraré cómo cifrar la contraseña.


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

Las variables se establecen usando llaves dobles en los libros de jugadas.


Indicaremos los valores de las variables en el archivo de inventario:


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

Preste atención a la directiva [all:vars] : dice que el siguiente bloque de texto son variables (vars) y son aplicables a todos los hosts (todos).


La construcción de "{{ user_password | password_hash('sha512') }}" también "{{ user_password | password_hash('sha512') }}" interesante. El hecho es que ansible no establece el usuario a través de user_add como lo haría manualmente. Y guarda todos los datos directamente, por lo que también debemos convertir la contraseña a un hash por adelantado, lo que hace este comando.


Agreguemos nuestro usuario al grupo sudo. Sin embargo, antes de esto, debe asegurarse de que dicho grupo exista porque nadie hará esto por nosotros:


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

Es bastante simple, también tenemos un módulo de grupo para crear grupos, con una sintaxis muy similar a apt. Entonces es suficiente registrar este grupo para el usuario ( groups: "sudo" ).
También es útil agregar una clave ssh a este usuario para que podamos iniciar sesión sin contraseña:


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

En este caso, la construcción "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" es interesante: copia el contenido del archivo id_rsa.pub (su nombre puede diferir), es decir, la parte pública de la clave ssh y lo carga en la lista de claves autorizadas para el usuario en el servidor.


Roles


Las tres tareas para la creación se pueden usar fácilmente como un grupo de tareas, y sería bueno mantener este grupo separado del libro de jugadas principal para que no crezca demasiado. Hay roles para esto en ansible.
De acuerdo con la estructura de archivos indicada al principio, los roles deben colocarse en un directorio de roles separado, para cada rol: un directorio separado con el mismo nombre, dentro del directorio de tareas, archivos, plantillas, etc.
./ansible/roles/user/tasks/main.yml estructura del archivo: ./ansible/roles/user/tasks/main.yml (main es el archivo principal que se cargará y ejecutará cuando el rol esté conectado al libro de jugadas, se pueden conectar otros archivos de rol en él). Ahora puede transferir a este archivo todas las tareas relacionadas con el usuario:


 # 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 

En el libro de jugadas principal, debe especificar el uso de la función 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 

Además, puede tener sentido realizar una actualización del sistema antes que todas las demás tareas, para esto puede cambiar el nombre del bloque de tasks en el que están definidas en pre_tasks .


Configuración de Nginx


Nginx ya debería estar instalado con nosotros, debe configurarlo y ejecutarlo. Hagámoslo de inmediato en el papel. Crea una estructura de archivo:


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

Ahora necesitamos archivos y plantillas. La diferencia entre los dos es que los archivos ansibles se copian directamente, tal como están. Y las plantillas deben tener la extensión j2 y pueden usar los valores de las variables usando los mismos corchetes dobles.


main.yml nginx en el archivo main.yml . Para esto, tenemos un módulo systemd:


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

Aquí no solo decimos que nginx debe iniciarse (es decir, ejecutarlo), sino que inmediatamente decimos que debe estar habilitado.
Ahora copie los archivos 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 el archivo de configuración principal de nginx (puede tomarlo directamente del servidor o escribirlo usted mismo). Y también el archivo de configuración para nuestra aplicación en el directorio sites_available (esto no es necesario pero es útil). En el primer caso, usamos el módulo de copia para copiar archivos (el archivo debe estar en /ansible/roles/nginx/files/nginx.conf ). En el segundo, copie la plantilla, sustituyendo los valores de las variables. La plantilla debe estar en /ansible/roles/nginx/templates/my_app.j2 ). Y puede verse más o menos 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 a los insertos {{ app_name }} , {{ app_path }} , {{ server_name }} , {{ inventory_hostname }} : estas son todas las variables cuyos valores ansibles sustituirán en la plantilla antes de copiar. Esto es útil si usa el libro de jugadas para diferentes grupos de hosts. Por ejemplo, podemos complementar nuestro archivo 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 

Si ejecutamos nuestro libro de jugadas ahora, realizará las tareas especificadas para ambos hosts. Pero al mismo tiempo, para el host de preparación, las variables diferirán de la producción, y no solo en roles y libros de jugadas, sino también en configuraciones nginx. {{ inventory_hostname }} no necesita especificarse en el archivo de inventario; esta es una variable especial ansible y el host para el que se está ejecutando el libro de jugadas se almacena allí.
Si desea tener un archivo de inventario para varios hosts y ejecutar solo para un grupo, esto se puede hacer con el siguiente comando:


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

Otra opción es tener archivos de inventario separados para diferentes grupos. O puede combinar dos enfoques si tiene muchos hosts diferentes.


Volvamos a la configuración de nginx. Después de copiar los archivos de configuración, necesitamos crear un enlace simbólico en sitest_enabled en my_app.conf desde sites_available. Y reinicie 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 es simple: de nuevo, módulos ansibles con una sintaxis bastante estándar. Pero hay un punto. Reiniciar nginx cada vez no tiene sentido. Notó que no estamos escribiendo comandos de la forma: "haga esto así", la sintaxis se parece más a "esto debería tener este estado". Y a menudo así es como funciona ansible. Si el grupo ya existe o el paquete del sistema ya está instalado, ansible lo comprobará y omitirá la tarea. Además, los archivos no se copiarán si coinciden completamente con lo que ya está en el servidor. Podemos aprovechar esto y reiniciar nginx solo si se han cambiado los archivos de configuración. Hay una directiva de registro para esto:


 # 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 

Si cambia uno de los archivos de configuración, se realizará la copia y se registrará la variable restart_nginx . Y solo si esta variable se ha registrado, el servicio se reiniciará.


Bueno, por supuesto, debe agregar el rol nginx al libro de jugadas principal.


Configuración de Postgresql


Necesitamos habilitar postgresql con systemd, tal como lo hicimos con nginx, y también crear un usuario que usaremos para acceder a la base de datos y a la base de datos misma.
Cree el rol /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 }}" 

No describiré cómo agregar variables al inventario, esto ya se ha hecho muchas veces, así como la sintaxis de los módulos postgresql_db y postgresql_user. Se pueden encontrar más datos en la documentación. La directiva become_user: postgres más interesante become_user: postgres . El hecho es que, de manera predeterminada, solo el usuario postgres tiene acceso a la base de datos postgresql y solo localmente. Esta directiva nos permite ejecutar comandos en nombre de este usuario (a menos, por supuesto, que tengamos acceso).
Además, es posible que deba agregar una línea a pg_hba.conf para abrir el acceso a la base de datos para un nuevo usuario. Esto se puede hacer de la misma manera que cambiamos la configuración de nginx.


Y, por supuesto, debe agregar el rol de postgresql al libro de jugadas principal.


Instalar ruby ​​a través de rbenv


Ansible no tiene módulos para trabajar con rbenv, pero se instala clonando un repositorio git. Por lo tanto, esta tarea se convierte en la más no estándar. /ansible/roles/ruby_rbenv/main.yml el rol /ansible/roles/ruby_rbenv/main.yml para ella y comencemos a llenarlo:


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

Nuevamente usamos la directiva Become_user para trabajar desde debajo del usuario que creamos para estos fines. Dado que rbenv está instalado en su directorio de inicio, no globalmente. Y también usamos el módulo git para clonar el repositorio especificando repo y dest.


Luego, necesitamos registrar rbenv init en bashrc y agregar rbenv a PATH en el mismo lugar. Para esto, tenemos el 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 -)"' 

Luego instale ruby_build:


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

Y finalmente instalar ruby. Esto se hace a través de rbenv, es decir, solo un 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 

Decimos qué equipo ejecutar y cómo. Sin embargo, aquí nos encontramos con el hecho de que ansible no ejecuta el código contenido en bashrc antes de ejecutar los comandos. Por lo tanto, rbenv tendrá que definirse directamente en el mismo script.


El siguiente problema es que el comando de shell no tiene estado en términos de ansible. Es decir, una comprobación automática de si esta versión de ruby ​​está instalada o no no lo hará. Podemos hacerlo nosotros mismos:


 - 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 

Y queda por instalar el paquete:


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

Y nuevamente, agregue nuestro rol ruby_rbenv al libro de jugadas principal.


Archivos compartidos


En general, esta configuración podría completarse. Luego queda ejecutar capistrano y copiará el código en sí, creará los directorios necesarios y ejecutará la aplicación (si todo está configurado correctamente). Sin embargo, a menudo capistrano necesita archivos de configuración adicionales, como database.yml o .env copiarlos como archivos y plantillas para nginx. Solo hay una sutileza. Antes de copiar archivos, debe crear una estructura de directorio para ellos, algo como esto:


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

especificamos solo un directorio y ansible creará automáticamente el padre, si es necesario.


Bóveda Ansible


Ya nos hemos topado con el hecho de que los datos secretos como la contraseña del usuario pueden aparecer en las variables. Si creó un archivo .env para la aplicación y database.yml entonces debería haber aún más datos críticos. Sería bueno esconderlos de miradas indiscretas. La bóveda de Ansible se utiliza para esto.


/ansible/vars/all.yml un archivo para las variables /ansible/vars/all.yml (aquí puede crear diferentes archivos para diferentes grupos de hosts, al igual que en el archivo de inventario: production.yml, staging.yml, etc.).
En este archivo, debe transferir todas las variables que deben cifrarse con la sintaxis 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 

Entonces este archivo se puede cifrar con el comando:


 ansible-vault encrypt ./vars/all.yml 

Naturalmente, durante el cifrado será necesario establecer una contraseña para el descifrado. Puede ver lo que aparece dentro del archivo después de llamar a este comando.


Usando el ansible-vault decrypt archivo puede ser descifrado, modificado y luego encriptado nuevamente.


Para trabajar, descifrar el archivo no es necesario. Lo almacena en forma cifrada y ejecuta el libro de jugadas con el argumento --ask-vault-pass . Ansible le pedirá una contraseña, obtendrá las variables y completará las tareas. Todos los datos permanecerán encriptados.


Un comando completo para varios grupos de hosts y una bóveda ansible se vería así:


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

Y no te daré el texto completo de libros de jugadas y roles, escribe por ti mismo. Porque lo más sensible es esto: si no entiendes lo que hay que hacer, entonces él no lo hará.

Source: https://habr.com/ru/post/460769/


All Articles