Há não muito tempo, eu precisava escrever vários manuais ansíveis para preparar o servidor para implantar o aplicativo rails. E, surpreendentemente, não encontrei um simples manual passo a passo. Não queria copiar o manual de outra pessoa sem entender o que estava acontecendo e, como resultado, tive que ler a documentação, coletando tudo sozinho. Talvez alguém que eu possa ajudar a acelerar esse processo com este artigo.
A primeira coisa a entender é que o ansible fornece uma interface conveniente para executar uma lista predefinida de ações no (s) servidor (es) remoto (s) via SSH. Não há mágica aqui, você não pode instalar o plug-in e sair da caixa de inatividade zero com a implantação do seu aplicativo com janela de encaixe, monitoramento e outras vantagens. Para escrever um manual, você deve saber exatamente o que deseja fazer e como fazê-lo. Portanto, não gosto de playbooks prontos do github, nem de artigos como: "Copie e execute, ele funcionará".
Do que precisamos?
Como eu disse, para escrever um manual, você precisa saber o que deseja fazer e como fazê-lo. Vamos decidir o que precisamos. Para uma aplicação Rails, precisaremos de vários pacotes de sistema: nginx, postgresql (redis, etc). Além disso, precisamos do ruby de uma determinada versão. É melhor instalá-lo através do rbenv (rvm, asdf ...). Executar tudo isso a partir da raiz do usuário é sempre uma má ideia, portanto, você precisa criar um usuário separado e configurar os direitos para ele. Depois disso, você precisa fazer o upload do nosso código para o servidor, copiar as configurações do nginx, postgres, etc e executar todos esses serviços.
Como resultado, a sequência de ações é a seguinte:
- Entrar como root
- instalar pacotes do sistema
- crie um novo usuário, configure direitos, chave ssh
- configurar pacotes do sistema (nginx etc) e executá-los
- Crie um usuário no banco de dados (você pode criar imediatamente um banco de dados)
- Efetue login como um novo usuário
- Instale rbenv e ruby
- Instale o bundler
- Preencha o código do aplicativo
- Iniciamos o servidor Puma
Além disso, os últimos passos podem ser feitos usando o capistrano, pelo menos ela pode copiar o código nos diretórios da versão da caixa, alternar a versão com um link simbólico em uma implantação bem-sucedida, copiar configurações do diretório compartilhado, reiniciar o puma etc. Tudo isso pode ser feito com o Ansible, mas por quê?
Estrutura de arquivo
O Ansible possui uma estrutura de arquivos rigorosa para todos os seus arquivos, portanto, é melhor mantê-lo em um diretório separado. E não é tão importante se será no próprio aplicativo de trilhos ou separadamente. Você pode armazenar arquivos em um repositório git separado. Pessoalmente, foi mais conveniente para mim criar o diretório ansible em / config do diretório rails do aplicativo e armazenar tudo em um repositório.
Manual simples
Playbook é um arquivo yml que descreve o que e como deve ser feito usando uma sintaxe especial. Vamos criar o primeiro manual que não faz nada:
--- - name: Simple playbook hosts: all
Aqui, simplesmente dizemos que nosso playbook se chama Simple Playbook
e que seu conteúdo deve ser executado para todos os hosts. Podemos salvá-lo no diretório / ansible com o nome playbook.yml
e tentar executar:
ansible-playbook ./playbook.yml PLAY [Simple Playbook] ************************************************************************************************************************************ skipping: no hosts matched
A Ansible diz que não conhece hosts que correspondam à lista all. Eles devem estar listados em um arquivo de inventário especial.
Vamos criá-lo no mesmo diretório ansible:
123.123.123.123
Portanto, basta especificar o host (idealmente, o host do seu VPS para testes, ou você pode registrar o host local) e salve-o no nome de inventory
.
Você pode tentar executar o ansible com um arquivo invetório:
ansible-playbook ./playbook.yml -i inventory PLAY [Simple Playbook] ************************************************************************************************************************************ TASK [Gathering Facts] ************************************************************************************************************************************ PLAY RECAP ************************************************************************************************************************************
Se você tiver acesso ssh ao host especificado, o ansible conectará e coletará informações sobre o sistema remoto. (TAREFA padrão [Reunindo fatos]), após o qual apresentará um breve relatório de progresso (PLAY RECAP).
Por padrão, o nome de usuário no qual você está conectado ao sistema é usado para a conexão. Provavelmente não estará no host. No arquivo playbook, você pode especificar qual usuário usar para se conectar usando a diretiva remote_user. Além disso, as informações sobre um sistema remoto geralmente podem ser desnecessárias para você e você não deve perder tempo coletando-o. Você também pode desativar esta tarefa:
--- - name: Simple playbook hosts: all remote_user: root become: true gather_facts: no
Tente executar o manual novamente e verifique se a conexão está funcionando. (Se você especificou a raiz do usuário, também deve especificar a diretiva torne-se: verdadeira para obter direitos elevados. Como a documentação diz: become set to 'true'/'yes' to activate privilege escalation.
- become set to 'true'/'yes' to activate privilege escalation.
Embora não esteja claro o porquê) .
Talvez você receba um erro causado pelo ansible que o intérprete python não pode determinar e especifique-o manualmente:
ansible_python_interpreter: /usr/bin/python3
onde você tem python pode ser encontrado com o comando whereis python
.
Instalar pacotes do sistema
O Ansible vem com muitos módulos para trabalhar com vários pacotes do sistema, portanto, não precisamos escrever scripts bash por qualquer motivo. Agora precisamos de um desses módulos para atualizar o sistema e instalar pacotes do sistema. Eu tenho o Ubuntu Linux no VPS, respectivamente, para instalar pacotes, uso o apt-get
e um módulo para ele . Se você usa um sistema operacional diferente, pode precisar de um módulo diferente (lembre-se, eu disse no início que precisamos saber com antecedência o que e como faremos). No entanto, é provável que a sintaxe seja semelhante.
Complementamos nosso manual de instruções 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: present
Tarefa é apenas a tarefa que o ansible executará em servidores remotos. Atribuímos um nome à tarefa para rastrear seu progresso no log. E descrevemos, usando a sintaxe de um módulo específico, o que ele precisa fazer. Nesse caso, apt: update_cache=yes
- diz para atualizar os pacotes do sistema usando o módulo apt. A segunda equipe é um pouco mais complicada. Passamos a lista de pacotes para o módulo apt e dizemos que seu state
deve se tornar present
, ou seja, dizemos para instalar esses pacotes. Da mesma forma, podemos dizer a eles para serem excluídos ou atualizados, simplesmente alterando o state
. Observe que, para o rails funcionar com o postgresql, precisamos do pacote postgresql-contrib que estamos instalando atualmente. Novamente, isso precisa ser conhecido e feito, e, por si só, o ansible não fará isso.
Tente executar o playbook novamente e verifique se os pacotes estão instalados.
Criação de novos usuários.
Para trabalhar com usuários, o Ansible também possui um módulo - usuário. Adicione outra tarefa (ocultei as partes já conhecidas do manual por trás dos comentários, para não copiá-lo totalmente todas as vezes):
--- - name: Simple playbook # ... tasks: # ... - name: Add a new user user: name: my_user shell: /bin/bash password: "{{ 123qweasd | password_hash('sha512') }}"
Criamos um novo usuário, definimos uma senha e uma senha. E então nos deparamos com vários problemas. E se os nomes de usuário precisarem ser diferentes para hosts diferentes? Sim, e manter a senha aberta no manual é uma péssima idéia. Para começar, colocaremos o nome de usuário e a senha em variáveis e, no final do artigo, mostrarei como criptografar a senha.
--- - name: Simple playbook # ... tasks: # ... - name: Add a new user user: name: "{{ user }}" shell: /bin/bash password: "{{ user_password | password_hash('sha512') }}"
As variáveis são definidas usando colchetes duplos nos playbooks.
Indicaremos os valores das variáveis no arquivo de inventário:
123.123.123.123 [all:vars] user=my_user user_password=123qweasd
Preste atenção à diretiva [all:vars]
- diz que o próximo bloco de texto são variáveis (vars) e são aplicáveis a todos os hosts (todos).
A construção de "{{ user_password | password_hash('sha512') }}"
também "{{ user_password | password_hash('sha512') }}"
interessante. O fato é que o ansible não define o usuário através de user_add
como você faria manualmente. E ele salva todos os dados diretamente, e é por isso que também devemos converter a senha em um hash antecipadamente, como esse comando.
Vamos adicionar nosso usuário ao grupo sudo. No entanto, antes disso, você precisa garantir que esse grupo exista, porque ninguém fará isso 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"
É bastante simples, também temos um módulo de grupo para criar grupos, com uma sintaxe muito semelhante ao apt. Então é suficiente registrar esse grupo no usuário ( groups: "sudo"
).
Também é útil adicionar uma chave ssh a esse usuário, para que possamos efetuar login sem uma senha:
--- - 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
Nesse caso, a construção "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
é interessante - ela copia o conteúdo do arquivo id_rsa.pub (seu nome pode ser diferente), ou seja, a parte pública da chave ssh e carrega-o na lista de chaves autorizadas para o usuário no servidor.
Funções
Todas as três tarefas para criação podem ser facilmente usadas como um grupo de tarefas, e seria bom manter esse grupo separado do manual principal para que ele não cresça muito. Existem papéis para isso em ansible.
De acordo com a estrutura de arquivos indicada no início, as funções devem ser colocadas em um diretório de funções separado, para cada função - um diretório separado com o mesmo nome, dentro do diretório de tarefas, arquivos, modelos, etc.
Vamos criar a estrutura do arquivo: ./ansible/roles/user/tasks/main.yml
(main é o arquivo principal que será carregado e executado quando a função estiver conectada à playbook, outros arquivos de função poderão ser conectados nela). Agora você pode 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: present
No manual principal, você deve especificar para usar a função de usuário:
--- - 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
Além disso, pode fazer sentido executar uma atualização do sistema antes de todas as outras tarefas, para isso, você pode renomear o bloco de tasks
no qual elas são definidas nas pre_tasks
.
Configuração do Nginx
O Nginx já deve estar instalado conosco, você precisa configurá-lo e executá-lo. Vamos fazer isso imediatamente no papel. Crie uma estrutura de arquivos:
- ansible - roles - nginx - files - tasks - main.yml - templates
Agora precisamos de arquivos e modelos. A diferença entre os dois é que os arquivos ansible são copiados diretamente, como estão. E os modelos devem ter a extensão j2 e podem usar os valores das variáveis usando os mesmos colchetes duplos.
Vamos incluir o nginx no arquivo main.yml
. Para isso, temos um módulo systemd:
# Copy nginx configs and start it - name: enable service nginx and start systemd: name: nginx state: started enabled: yes
Aqui, não apenas dizemos que o nginx deve ser iniciado (ou seja, executa-o), mas também dizemos imediatamente que ele deve estar ativado.
Agora copie 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'
Criamos o arquivo de configuração principal do nginx (você pode acessá-lo diretamente do servidor ou gravá-lo). E também o arquivo de configuração para o nosso aplicativo no diretório sites_available (isso não é necessário, mas útil). No primeiro caso, usamos o módulo de cópia para copiar arquivos (o arquivo deve estar em /ansible/roles/nginx/files/nginx.conf
). No segundo - copie o modelo, substituindo os valores das variáveis. O modelo deve estar em /ansible/roles/nginx/templates/my_app.j2
). E pode ser algo como 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 }}; .... }
Preste atenção às inserções {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
- essas são todas as variáveis cujos valores ansible serão substituídos no modelo antes de copiar. Isso é útil se você usar o manual para diferentes grupos de hosts. Por exemplo, podemos complementar nosso arquivo de inventário:
[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 executarmos agora o nosso manual, ele executará as tarefas especificadas para os dois hosts. Mas, ao mesmo tempo, para o host intermediário, as variáveis diferem da produção, e não apenas nas funções e playbooks, mas também nas configurações do nginx. {{ inventory_hostname }}
nome_do_host_do_ {{ inventory_hostname }}
não precisa ser especificado no arquivo de inventário - essa é uma variável especial ansible e o host para o qual o manual está atualmente em execução é armazenado lá.
Se você deseja ter um arquivo de inventário para vários hosts e executar apenas um grupo, isso pode ser feito com o seguinte comando:
ansible-playbook -i inventory ./playbook.yml -l "staging"
outra opção é ter arquivos de inventário separados para diferentes grupos. Ou você pode combinar duas abordagens se tiver muitos hosts diferentes.
Vamos voltar à configuração do nginx. Após copiar os arquivos de configuração, precisamos criar um link simbólico no sitest_enabled no my_app.conf em sites_available. E reinicie o 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
Tudo é simples aqui - novamente, módulos ansible com sintaxe bastante padrão. Mas há um ponto. Reiniciar o nginx toda vez não faz sentido. Você notou que não estamos escrevendo comandos do formulário: "faça assim", a sintaxe se parece mais com "isso deve ter esse estado". E na maioria das vezes é assim que funciona o ansible. Se o grupo já existir ou o pacote do sistema já estiver instalado, o ansible verificará isso e pulará a tarefa. Além disso, os arquivos não serão copiados se coincidirem completamente com o que já está no servidor. Podemos tirar proveito disso e reiniciar o nginx somente se os arquivos de configuração foram alterados. Existe uma diretiva de registro para isso:
# 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 um dos arquivos de configuração mudar, a cópia será executada e a variável restart_nginx
será registrada. E somente se essa variável tiver sido registrada, o serviço será reiniciado.
Bem, é claro, você precisa adicionar a função nginx ao manual principal.
Configuração do Postgresql
Precisamos ativar o postgresql com systemd, assim como fizemos com o nginx, e também criar um usuário que usaremos para acessar o banco de dados e o próprio banco de dados.
Crie a função /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 descreverei como adicionar variáveis ao inventário, isso já foi feito várias vezes, bem como a sintaxe dos módulos postgresql_db e postgresql_user. Mais dados podem ser encontrados na documentação. A diretiva become_user: postgres
mais interessante become_user: postgres
. O fato é que, por padrão, apenas o usuário do postgres tem acesso ao banco de dados postgresql e somente localmente. Essa diretiva nos permite executar comandos em nome desse usuário (a menos que tenhamos acesso).
Além disso, pode ser necessário adicionar uma linha ao pg_hba.conf para abrir o acesso ao banco de dados para um novo usuário. Isso pode ser feito da mesma maneira que alteramos a configuração do nginx.
E é claro que você precisa adicionar o papel do postgresql ao manual principal.
Instale o ruby através do rbenv
O Ansible não possui módulos para trabalhar com o rbenv, mas é instalado clonando um repositório git. Portanto, essa tarefa se torna a mais não-padrão. Vamos criar a função /ansible/roles/ruby_rbenv/main.yml
para ela e começar a preenchê-la:
# Install rbenv and ruby - name: Install rbenv become_user: "{{ user }}" git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv
Novamente usamos a diretiva become_user para trabalhar com o usuário que criamos para esses fins. Como o rbenv está instalado em seu diretório inicial, não em todo o mundo. E também usamos o módulo git para clonar o repositório especificando repo e dest.
Em seguida, precisamos registrar o rbenv init no bashrc e adicionar o rbenv ao PATH no mesmo local. Para isso, 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 -)"'
Em seguida, instale o 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, instale o ruby. Isso é feito via rbenv, ou seja, apenas um 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
Dizemos qual equipe executar e como. No entanto, aqui nos deparamos com o fato de o ansible não executar o código contido no bashrc antes de executar os comandos. Portanto, o rbenv terá que ser definido diretamente no mesmo script.
O próximo problema é que o comando shell não tem estado em termos de ansible. Ou seja, uma verificação automática se esta versão do ruby está instalada ou não. Nós podemos fazer isso sozinhos:
- 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
E resta instalar o bundler:
- name: Install bundler become_user: "{{ user }}" shell: | export PATH="${HOME}/.rbenv/bin:${PATH}" eval "$(rbenv init -)" gem install bundler
E, novamente, adicione nossa função ruby_rbenv ao manual principal.
Arquivos compartilhados.
Em geral, essa configuração pode ser concluída. Resta então rodar o capistrano e ele copiará o próprio 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
pode copiá-los exatamente como arquivos e modelos para o nginx. Existe apenas uma sutileza. Antes de copiar arquivos, você precisa criar uma 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: directory
especificamos apenas um diretório e o ansible criará automaticamente o pai, se necessário.
Cofre Ansible
Já descobrimos que dados secretos, como senha de usuário, podem aparecer em variáveis. Se você criou um arquivo .env
para o aplicativo e database.yml
, deve haver ainda mais esses dados críticos. Seria bom escondê-los de olhares indiscretos. O cofre Ansible é usado para isso.
Vamos criar um arquivo para as variáveis /ansible/vars/all.yml
(aqui você pode criar arquivos diferentes para diferentes grupos de hosts, assim como no arquivo de inventário: production.yml, staging.yml, etc).
Nesse arquivo, você precisa transferir todas as variáveis que devem ser criptografadas usando a sintaxe yml padrão:
# 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
Então este arquivo pode ser criptografado com o comando:
ansible-vault encrypt ./vars/all.yml
Naturalmente, durante a criptografia, será necessário definir uma senha para descriptografia. Você pode ver o que aparece dentro do arquivo depois de chamar este comando.
Usando ansible-vault decrypt
arquivo pode ser descriptografado, modificado e depois criptografado novamente.
Para funcionar, descriptografar o arquivo não é necessário. Você o armazena na forma criptografada e executa o manual com o argumento --ask-vault-pass
. O Ansible solicitará uma senha, obterá as variáveis e concluirá as tarefas. Todos os dados permanecerão criptografados.
Um comando completo para vários grupos de hosts e o cofre ansible seria algo parecido com isto:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
E não vou lhe dar o texto completo de playbooks e papéis, escreva para si mesmo. Porque o mais ansioso é este - se você não entender o que precisa ser feito, ele não o fará.