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:
- Inicie sesión como root
- instalar paquetes do sistema
- crear un novo usuario, configurar dereitos, clave ssh
- configurar paquetes do sistema (nginx, etc.) e executalos
- Creamos un usuario na base de datos (podes crear inmediatamente unha base de datos)
- Inicia sesión como usuario novo
- Instala rbenv e ruby
- Instalación do bundler
- Cargando o código da aplicación
- 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
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
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
и
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
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
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
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