Recentemente, eu precisei escrever alguns playbooks do Ansible para preparar o servidor para a implantação de um aplicativo Rails. E, para minha surpresa, não encontrei um manual passo a passo simples. Eu não queria copiar o playbook de outra pessoa sem entender o que estava acontecendo e, no final, tive que ler a documentação, reunindo tudo sozinho. Talvez eu possa ajudar alguém a acelerar esse processo com este artigo.
Primeiramente, é importante entender que o Ansible oferece uma interface conveniente para executar uma lista predefinida de ações em um servidor remoto (ou servidores) via SSH. Não há nenhuma mágica aqui, não é possível instalar um plugin e obter automaticamente a implantação da sua aplicação com zero downtime usando Docker, monitoramento e outros recursos. Para escrever um playbook, você deve saber exatamente o que deseja fazer e como fazê-lo. Portanto, não estou satisfeito com playbooks prontos do GitHub, ou artigos do tipo: “Copie e execute, - vai funcionar.”
O que precisamos?
Como já mencionei, para escrever um playbook você precisa saber o que quer fazer e como fazê-lo. Vamos esclarecer o que precisamos. Para um aplicativo Rails, precisaremos de alguns pacotes de sistema: nginx, postgresql (redis, etc.). Além disso, precisamos de uma versão específica do Ruby. O melhor é instalá-lo via rbenv (rvm, asdf…). Executar tudo isso como usuário root é sempre uma má ideia, então precisamos criar um usuário separado e configurar suas permissões. Depois disso, é necessário enviar nosso código para o servidor, copiar as configurações para nginx, postgres, etc. e iniciar todos esses serviços.
Assim, a sequência de ações é a seguinte:
- Fazemos login como root
- instalamos os pacotes de sistema
- criamos um novo usuário, configuramos permissões e chave SSH
- configuramos os pacotes de sistema (nginx, etc.) e os iniciamos
- Criamos um usuário no banco de dados (podemos criar o banco de dados ao mesmo tempo)
- Fazemos login com o novo usuário
- Instalamos rbenv e Ruby
- Instalamos o Bundler
- Carregamos o código do aplicativo
- Iniciamos o servidor Puma
Além disso, as etapas finais podem ser feitas usando capistrano, pois ela já vem com a capacidade de copiar código para diretórios de release, alternar o release via symlink após um deploy bem-sucedido, copiar as configurações do diretório compartilhado, reiniciar o puma, etc. Tudo isso também pode ser feito com Ansible, mas por que fazer?
Estrutura de Arquivos
Ansible tem uma estrutura de arquivos rigorosa Playbook Simples
Um playbook é um arquivo yml que descreve, usando uma sintaxe especial, o que e como o ansible deve fazer. Vamos criar nosso primeiro playbook que não faz nada:
--- - name: Playbook Simples hosts: all
Aqui nós simplesmente dizemos que nosso playbook se chamae que seu conteúdo deve ser executado para todos os hosts. Podemos salvá-lo no diretório /ansible com o nome Um playbook é um arquivo yml que descreve, usando uma sintaxe especial, o que e como o ansible deve fazer. Vamos criar nosso primeiro playbook que não faz nada: playbook.yml playbook.yml e tentar executar:
ansible-playbook ./playbook.yml
PLAY [Playbook Simples] ************************************************************************************************************************************
skipping: nenhum host correspondeuAnsible informa que não conhece hosts que correspondam à lista all. Eles devem ser listados em um arquivo de .
Vamos criar um neste mesmo diretório ansible:
123.123.123.123Dessa forma, basta indicar o host (de preferência o host do seu VPS para testes, ou você pode escrever localhost) e salvá-lo com o nome inventory.
Você pode tentar executar o ansible com o arquivo de inventário:
ansible-playbook ./playbook.yml -i inventory
PLAY [Playbook Simples] ************************************************************************************************************************************
TASK [Coletando Fatos] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************Se você tiver acesso por ssh ao host indicado, o ansible se conectará e coletará informações sobre o sistema remoto. (Tarefa padrão [Coletando Fatos]) e em seguida fornecerá um resumo da execução (PLAY RECAP).
Por padrão, o nome de usuário usado para a conexão é aquele com o qual você está logado no sistema. No host, provavelmente, ele não estará disponível. No arquivo do playbook, você pode especificar qual usuário usar para a conexão através da diretiva remote_user. Assim, as informações sobre o sistema remoto podem não ser necessárias e não vale a pena gastar tempo coletando-as. Essa tarefa também pode ser desativada:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: noTente executar o playbook novamente e certifique-se de que a conexão está funcionando. (Se você indicou o usuário root, também deve adicionar a diretiva become: true para obter permissões elevadas. Como está escrito na documentação: become set to 'true'/'yes' to activate privilege escalation. embora não esteja muito claro por quê).
É possível que você receba um erro causado pelo fato de que o ansible não consegue determinar o interpretador Python; você pode especificá-lo manualmente:
ansible_python_interpreter: /usr/bin/python3 onde você pode descobrir onde está o Python com o comando whereis python.
Instalação de pacotes do sistema
Na instalação padrão do Ansible, há muitos módulos para trabalhar com vários pacotes de sistema, o que nos impede de escrever scripts em bash para qualquer situação. Agora, precisaremos de um desses módulos para atualizar o sistema e instalar pacotes do sistema. No meu VPS, tenho Ubuntu Linux, portanto, para instalar pacotes, uso apt-get e . Se você estiver usando um sistema operacional diferente, pode ser que precise de outro módulo (lembre-se de que no início eu disse que você deve saber com antecedência o que e como faremos). No entanto, a sintaxe provavelmente será semelhante.
Vamos complementar nosso playbook com as 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: presentA tarefa — é exatamente essa tarefa que o ansible executará em servidores remotos. Damos um nome à tarefa para rastrear sua execução no log. E descrevemos, utilizando a sintaxe de um módulo específico, o que ele precisa fazer. Neste caso apt: update_cache=yes — diz para atualizar os pacotes do sistema usando o módulo apt. O segundo comando é um pouco mais complexo. Passamos para o módulo apt uma lista de pacotes e dizemos que eles state devem se tornar present, ou seja, estamos dizendo para instalar esses pacotes. Da mesma forma, podemos dizer para removê-los ou atualizá-los, basta alterar. state. Observe que, para o funcionamento do rails com postgresql, precisamos do pacote postgresql-contrib, que estamos instalando agora. É importante saber disso e fazer, o ansible não fará isso automaticamente.
Tente executar o playbook novamente e verifique se os pacotes serão instalados.
Criação de novos usuários.
Para trabalhar com usuários, o Ansible também possui um módulo — user. Vamos adicionar mais uma task (já escondi partes conhecidas do playbook em comentários, para não precisar copiá-lo inteiro todas as vezes):
---
- nome: Playbook simples
# ...
tasks:
# ...
- nome: Adicionar um novo usuário
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"Estamos criando um novo usuário, definindo seu shell e senha. E logo encontramos alguns problemas. E se os nomes dos usuários precisarem ser diferentes para diferentes hosts? Além disso, armazenar a senha em texto claro no playbook é uma ideia muito ruim. Primeiro, vamos mover o nome de usuário e a senha para variáveis, e mais para o final do artigo, mostrarei como criptografar a senha.
---
- nome: Playbook simples
# ...
tasks:
# ...
- nome: Adicionar um novo usuário
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"Usando chaves duplas nos playbooks para definir variáveis.
Os valores das variáveis serão especificados no arquivo de inventory:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasdObserve a diretiva [all:vars] — ela indica que o próximo bloco de texto é de variáveis (vars) e são aplicáveis a todos os hosts (all).
A construção também é interessante "{{ user_password | password_hash('sha512') }}". A questão é que o ansible não adiciona o usuário através do user_add como você faria manualmente. Ele salva todos os dados diretamente, razão pela qual devemos converter a senha em hash previamente, e é isso que esse comando faz.
Vamos adicionar nosso usuário ao grupo sudo. No entanto, antes disso, precisamos garantir que tal grupo existe, pois ninguém fará isso por nós:
---
- nome: Playbook simples
# ...
tasks:
# ...
- nome: Garantir que um grupo 'sudo' exista
group:
name: sudo
state: present
- nome: Adicionar um novo usuário
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"Tudo é bastante simples, também temos um módulo group para criar grupos, com uma sintaxe muito parecida com a do apt. Depois disso, basta atribuir esse grupo ao usuário (groups: "sudo").
Também é útil adicionar uma chave ssh a esse usuário, para que possamos fazer login como ele sem senha:
---
- name: Playbook simples
# ...
tasks:
# ...
- name: Garantir um grupo 'sudo'
group:
name: sudo
state: present
- name: Adicionar um novo usuário
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Implantar chave SSH
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: presentNeste caso, a construção que é interessante "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — ela copia o conteúdo do arquivo id_rsa.pub (o nome pode ser diferente), ou seja, a parte pública da chave ssh e a carrega na lista de chaves autorizadas para o usuário no servidor.
Funções
As três tarefas para criar o usuário podem ser facilmente agrupadas em um único grupo de tarefas, e seria bom armazenar esse grupo separadamente do playbook principal, para que ele não cresça demais. Para isso, o Ansible possui .
De acordo com a estrutura de arquivos mencionada no início, as roles devem ser colocadas em um diretório separado roles, com um diretório separado para cada role com o mesmo nome, dentro do diretório tarefas, arquivos, templates, etc.
Vamos criar a estrutura de arquivos: ./ansible/roles/user/tasks/main.yml (main — este é o arquivo principal que será carregado e executado ao conectar a role ao playbook, nele você pode incluir outros arquivos da role). Agora podemos transferir para este arquivo todas as tarefas relacionadas ao usuário:
# 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: presentNo playbook principal, é necessário indicar que se deve usar a role user:
---
- name: Playbook simples
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Atualizar sistema
apt: update_cache=yes
- name: Instalar dependências do sistema
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
roles:
- userAlém disso, talvez faça sentido realizar a atualização do sistema antes de todas as outras tarefas, para isso você pode renomear o bloco tasks no qual elas são definidas em pre_tasks.
Configuração do nginx
O Nginx já deve estar instalado, é necessário configurá-lo e iniciá-lo. Vamos fazer isso diretamente na role. Criamos a estrutura de arquivos:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templatesAgora precisamos dos arquivos e dos templates. A diferença entre eles é que os arquivos são copiados diretamente pelo Ansible, conforme estão. Já os templates devem ter a extensão j2 e podem usar os valores das variáveis através das mesmas chaves duplas.
Vamos incluir o nginx no main.yml arquivo. Para isso, temos o módulo systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yesAqui, não apenas dizemos que o nginx deve ser iniciado (ou seja, o estamos iniciando), mas também informamos que ele deve ser habilitado.
Agora vamos copiar os arquivos de configuração:
# 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'Estamos criando o arquivo de configuração principal do nginx (podemos pegá-lo diretamente do servidor ou escrever um novo). E também o arquivo de configuração para nosso aplicativo na diretório sites_available (isso não é obrigatório, mas é útil). No primeiro caso, usamos o módulo copy para copiar arquivos (o arquivo deve estar em /ansible/roles/nginx/files/nginx.conf). No segundo - copiamos o template, substituindo os valores das variáveis. O template deve estar em /ansible/roles/nginx/templates/my_app.j2). E ele pode ser parecido com isto:
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 }};
....
}Note as inserções {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — todas essas são variáveis, cujos valores o Ansible irá substituir no template antes da cópia. Isso é útil quando se utiliza um playbook para diferentes grupos de hosts. Por exemplo, podemos complementar nosso arquivo de inventory:
[production]
123.123.123.123
[staging]
231.231.231.231
[all:vars]
user=my_user
user_password=123qweasd
[production:vars]
server_name=production
app_path=\/home\/www\/my_app
app_name=my_app
[staging:vars]
server_name=staging
app_path=\/home\/www\/my_stage
app_name=my_stage_appSe agora executarmos nosso playbook, ele irá realizar as tarefas especificadas para ambos os hosts. Mas as variáveis para o host de staging serão diferentes da de production, não apenas nas roles e playbooks, mas também nas configurações do nginx. {{ inventory_hostname }} não é necessário especificar no arquivo inventory — isso é e ela armazena o host para o qual o playbook está sendo executado no momento.
Se você quiser ter um arquivo inventory para vários hosts, mas executar apenas para um único grupo, isso pode ser feito com o seguinte comando:
ansible-playbook -i inventory .\/playbook.yml -l "staging"outra opção — ter arquivos inventory separados para diferentes grupos. Ou você pode combinar os dois métodos, se tiver muitos hosts diferentes.
Vamos voltar à configuração do nginx. Após copiar os arquivos de configuração, precisamos criar um symlink em sites_enabled para my_app.conf a partir de sites_available. E reiniciar o nginx.
... # código antigo em mail.yml
- name: Criar symlink para sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: reiniciar nginx
service:
name: nginx
state: restartedAqui tudo é simples — são novamente os módulos do ansible com uma sintaxe bastante padrão. Mas há um ponto. Não faz sentido reiniciar o nginx toda vez. Você notou que não escrevemos comandos do tipo: "faça isso assim", a sintaxe é mais como "isso deve ter este estado aqui". E na maioria das vezes, é assim que o ansible funciona. Se o grupo já existir, ou se o pacote do sistema já estiver instalado, o ansible verificará isso e ignorará a tarefa. Da mesma forma, os arquivos não serão copiados se coincidirem completamente com o que já existe no servidor. Podemos tirar proveito disso e reiniciar o nginx apenas se os arquivos de configuração forem modificados. Para isso, existe a diretiva register:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
- name: Copy the nginx.conf
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
register: restart_nginx
- name: Copy template my_app.conf
template:
src: my_app_conf.j2
dest: /etc/nginx/sites-available/my_app.conf
owner: root
group: root
mode: '0644'
register: restart_nginx
- name: Create symlink to sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: restart nginx
service:
name: nginx
state: restarted
when: restart_nginx.changedSe um dos arquivos de configuração mudar, a cópia será realizada e a variável será registrada. restart_nginx. E somente se essa variável estiver registrada, o reinício do serviço será realizado.
E, claro, é necessário adicionar o papel do nginx ao playbook principal.
Configuração do postgresql
Precisamos ativar o postgresql através do systemd da mesma forma que fizemos com o nginx, bem como criar um usuário que utilizaremos para acessar o banco de dados e o próprio banco de dados.
Vamos criar um 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 }}"Não vou detalhar como adicionar variáveis ao inventory, isso já foi feito muitas vezes, assim como a sintaxe dos módulos postgresql_db e postgresql_user. Mais informações podem ser encontradas na documentação. Aqui, o mais interessante é a diretiva become_user: postgres. O fato é que, por padrão, o acesso ao banco de dados postgresql é restrito apenas ao usuário postgres e apenas localmente. Esta diretiva nos permite executar comandos em nome desse usuário (desde que tenhamos acesso).
Além disso, pode ser necessário adicionar uma linha no pg_hba.conf para abrir o acesso ao novo usuário no banco. Isso pode ser feito da mesma forma que alteramos a configuração do nginx.
E, claro, é necessário adicionar o papel do postgresql ao playbook principal.
Instalação do ruby via rbenv
No ansible não há módulos para trabalhar com rbenv, e ele é instalado clonando o repositório do git. Portanto, essa tarefa se torna a mais não padrão. Vamos criar um papel para isso /ansible/roles/ruby_rbenv/main.yml e começar a preenchê-lo:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenvNovamente usamos a diretiva become_user para trabalhar sob o usuário que criamos para esse propósito. Como o rbenv é instalado em seu diretório home, e não globalmente. Também usamos o módulo git para clonar o repositório, especificando repo e dest.
Em seguida, precisamos adicionar rbenv init no bashrc e também adicionar rbenv no PATH. Para isso, temos o módulo lineinfile:
- name: Adicionar rbenv ao PATH
become_user: "{{ user }}"
lineinfile:
path: ~\/ .bashrc
state: present
line: 'export PATH="${HOME}\/ .rbenv\/bin:${PATH}"'
- name: Adicionar rbenv init ao bashrc
become_user: "{{ user }}"
lineinfile:
path: ~\/ .bashrc
state: present
line: 'eval "$(rbenv init -)"'Depois, precisamos instalar o ruby_build:
- name: Instalar ruby-build
become_user: "{{ user }}"
git: repo=https:\/\/github.com\/rbenv\/ruby-build.git dest=~\/ .rbenv\/plugins\/ruby-buildE, finalmente, instalar o ruby. Isso é feito através do rbenv, ou seja, apenas com um comando bash:
- name: Instalar ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}\/ .rbenv\/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: \/bin\/bashDizemos qual comando executar e como. No entanto, aqui encontramos que o ansible não executa o código contido no bashrc antes de executar os comandos. Portanto, teremos que definir rbenv diretamente neste mesmo script.
O próximo problema está relacionado ao fato de que o comando shell não possui estado do ponto de vista do ansible. Ou seja, não haverá verificação automática se essa versão do ruby está instalada ou não — teremos que fazer isso nós mesmos:
- name: Instalar 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\/bashE resta instalar o bundler:
- name: Instalar bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}\/ .rbenv\/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundlerE novamente adicionar nosso papel ruby_rbenv ao playbook principal.
Arquivos compartilhados.
No geral, nessa configuração poderíamos encerrar. A seguir, basta iniciar o capistrano e ele mesmo copiará o código, criará os diretórios necessários e iniciará o aplicativo (se tudo estiver configurado corretamente). No entanto, muitas vezes o capistrano precisa de arquivos de configuração adicionais, como database.yml ou .env Eles podem ser copiados exatamente como arquivos e modelos para nginx. Há apenas um pequeno detalhe. Antes de copiar os arquivos, é necessário criar a estrutura de diretórios para eles, algo como isto:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directoryNós especificamos apenas um diretório e o Ansible automaticamente criará os pais, se necessário.
Ansible Vault
Já encontramos situações em que variáveis podem conter dados secretos, como senhas de usuário. Se você criou .env um arquivo para o aplicativo, e database.yml lá, deve haver ainda mais dados críticos. Seria bom ocultá-los de olhos curiosos. Para isso, usamos .
Vamos criar um arquivo para variáveis /ansible/vars/all.yml (aqui você pode criar arquivos diferentes para diferentes grupos de hosts, assim como no arquivo inventory: production.yml, staging.yml, etc.).
Neste arquivo, é necessário transferir todas as variáveis que devem ser criptografadas, usando a sintaxe padrão do 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_baseDepois disso, este arquivo pode ser criptografado com o comando:
ansible-vault encrypt ./vars/all.ymlNaturalmente, ao criptografar, você precisará estabelecer uma senha para descriptografar. Você pode ver o que haverá dentro do arquivo após executar este comando.
Com o ansible-vault decrypt o arquivo pode ser descriptografado, alterado e, em seguida, criptografado novamente.
Para funcionar, o arquivo não precisa ser descriptografado. Você o mantém em forma criptografada e executa o playbook com o argumento --ask-vault-pass. O Ansible pedirá a senha, recuperará as variáveis e executará as tarefas. Todos os dados permanecerão criptografados.
O comando completo para vários grupos de hosts e ansible vault ficará assim:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-passE eu não fornecerei o texto completo dos playbooks e funções, escreva você mesmo. Porque o Ansible é uma ferramenta assim — se você não entende o que deve fazer, ele também não fará por você.
Fonte: habr.com
