Como configurar o ambiente de desenvolvimento usando Ansible e Docker para provisionar containers

Publicado em 11 de dezembro de 2015 às 14:18

Eu já falei um pouco sobre o Docker aqui no blog, certo? Então não é segredo pra ninguém o quanto eu gosto dessa ferramenta e o quanto a acho útil para usar como ambiente de desenvolvimento e também para entregar sua aplicação.

Mas, e se o servidor que você for usar não suportar containerização? Ou se, de repente, você está apenas desenvolvendo a aplicação para um cliente que usa só um servidor virtualizado? Por fim, e se você precisar provisionar uma máquina como o seu container? Afinal, você não pode usar o Dockerfile pra isso. E é aí que entra a mágica do Ansible.

 

Ansible

Mas, pra início de conversa, o que é o Ansible? 
O Ansible é uma plataforma de provisionamento de máquinas, assim como o Chef e o Puppet. A sua vantagem? Não precisar de um client instalado na máquina que será provisionada de forma que, caso exista alguma limitação quanto a isso, ou se você quiser que a máquina siga à risca o que for escrito nos arquivos de provisionamento, você não terá problemas. 

Como ele consegue fazer isso? Ele usa o SSH para provisionar a máquina, ou seja, você instala o client na sua própria máquina, passa as informações de conexão para o Ansible e ele se conecta por SSH na máquina que será provisionada e executa os comandos.

SSH no Docker? NOOOOOO...

Calma, não precisa entrar em desespero, eu sei que usar o SSH no Docker não é a melhor ideia, até porque o container usado normalmente roda só um comando em substituição a um serviço. 
Mas, então, como vou provisionar o container? Boa notícia, o Ansible também pode ser rodado localmente
E esse é um dos segredos que torna possível provisionar um container com o Ansible. Explicaremos isso aqui hoje.

Primeiramente, como usar o Ansible?

O Ansible trabalha com playbooks. Estes playbooks são arquivos que você escreve com as diretrizes de provisionamento. Cada playbook possui "roles", que são efetivamente as informações de provisionamento. Após definir as roles, em um playbook você define em quais hosts uma role será executada. 
Cada role possui tasks (tarefas de provisionamento),  handlers (tarefas para manipular serviços e etc.), variáveis (valores que você define para serem usados seja nas tasks, nos handlers ou nos templates) e templates (arquivos para serem transformados em configurações dentro das máquinas, que podem inclusive conter variáveis). 

Vou explicar melhor cada um deles abaixo.

Estrutura de pastas

Inicialmente, precisamos entender como estruturar suas pastas e arquivos de maneira que o Ansible entenda.

├── .  
    ├── roles  
    │   ├── role  
    │       ├── handlers  
    │           └── main.yml  
    │       ├── tasks  
    │           └── main.yml  
    │       ├── templates  
    │           └── template.j2  
    │       └── vars  
    │           └── main.yml  
    └── playbook.yml  

Você precisa criar uma pasta na qual armazenará seus arquivos e pastas do Ansible. Dentro dessa pasta, você precisa ter o seu playbook, que pode ter qualquer nome e extensão .yml (o Ansible usa arquivos YAML) e a pasta roles, na qual você armazanerá as suas roles. Ah vá, Renato, sério?

Dentro da pasta roles, você cria uma pasta para cada role, nomeando a pasta com o nome da sua role.

Dentro da pasta da sua role, você cria uma pasta chamada handlers, uma chamada tasks, outra chamada templates e por último uma pasta chamada vars, ficando cada uma responsável por armazenar os arquivos .yml que lhe competem, baseadas em seus nomes.

Agora vamos lá, como escrever cada um desses arquivos?

Esse artigo começa a ficar interessante a partir daqui. Inicialmente, vamos entender como escrever os arquivos do Ansible? Aqui você precisa ter um conhecimento básico de como escrever um arquivo YAML, então te recomendo a acessar o yaml.org.

Vars

Vou começar pelo mais simples de todos, o arquivo de variáveis. 
Ele precisa apenas, que no formato YAML, você especifique o nome da variável e o conteúdo dela.

variavel: conteudo  

Este arquivo de exemplo foi usado no provisionamento de um container para banco de dados Mysql.

---
# The variables file used by the playbooks in the dbservers group.
# These don't have to be explicitly imported by vars_files: they are autopopulated.

# Mysql service name for drupal application
mysqlservice: mysqld

# Mysql port for drupal application
mysql_port: 3306


# Database name for the drupal application
dbname: databasename

# Mysql Username for drupal application
dbuser: root  
Handlers

O arquivo de Handlers, assim como todo o resto, utiliza Yaml. Num handler você define, do lado esquerdo o tipo de tarefa a ser executada (lidar com serviços, instalação de pacotes, uso de módulos, etc) e do lado passa os parâmetros. 
Você precisa também definir um nome para aquele handler, de modo que você possa invocá-lo em outros lugares. Um arquivo pode ter mais de um handler

- name: nome do handler
  tarefa: parâmetros

Como exemplo, o handler abaixo reinicia o Apache, de forma que quando eu executar uma tarefa que precise reiniciar o serviço, invoque-o.

- name: restart webserver
  service: name=apache2 state=restarted
Templates

Um template serve como base para criar um arquivo dentro da máquina. Usando filtros do Jinja2 você consegue até chamar as variáveis definidas nos arquivos da pasta vars e ainda aplicar alguma lógica baseada no conteúdo delas. 
Um template não tem muito segredo. Pra você criá-lo, apenas copie o arquivo que você iria inserir na mão e substitua os conteúdos (que você colocou em variáveis) pelo nome da variável cercada por {{ }}.

O arquivo abaixo, por exemplo, é um arquivo de configurações do Apache, chamando algumas variáveis (neste caso, domain, alias, httpd_port, etc).

<VirtualHost *:{{contact.httpd_port}}>

    ServerName {{contact.domain}}
    ServerAlias {{contact.alias}}
    DocumentRoot {{contact.path_projeto}}

    <Directory {{contact.path_projeto}} >
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Order allow,deny
        allow from all
    </Directory>

    # Possible values include: debug, info, notice, warn, error, crit,
    # alert, emerg.
    LogLevel debug

    ErrorLog ${APACHE_LOG_DIR}/prod__error.log
    CustomLog ${APACHE_LOG_DIR}/prod__access.log combined

    SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on

    <IfModule security2_module>
        SecAuditEngine RelevantOnly
        SecAuditLogRelevantStatus "^(?:5|4(?!04))"
    </IfModule>

    <Location />
        Order allow,deny
        Allow from all
    </Location>

</VirtualHost>  
Tasks

Essa é a parte mais importante de tudo isso. Nos arquivos de tasks, você define todas as tasks principais do provisionamento da sua máquina. Aqui você geralmente usará todas as informações dos arquivos explicados antes. Você usará variáveis, os handlers, os templates, etc. 

Um arquivo de task, também escrito em YAML, pode usar as variáveis (usando as duplas chaves, assim como no template), pode usar handlers, através do comando notify, pode rodar comandos, usar módulos do Ansible, instalar pacotes e muitas outras coisas, tudo depende do comando que você usar. 

Um padrão que eu gosto de usar, mas não é obrigatório, é separar as minhas tasks com base na sua função (por exemplo, uma para instalar o nginx e configurá-lo, outra para o php-fpm, outra para clonar o projeto, etc). Você cria estes arquivos, todos também em formato YAML, e aí no seu arquivo obrigatório main.yml você apenas usa o comando include. 
Abaixo fica um exemplo de como fiz isso para um projeto com Drupal. Primeiro exibirei o arquivo usado para instalação e configuração do Apache e no outro o main.yml, com os includes.

install_httpd.yml

---
# These tasks install http and the php modules.

- name: Install http and php etc
  apt: name={{ item }} state=present
  with_items:
   - apache2
   - php5
   - php-apc
   - php5-curl
   - php5-gd
   - php5-imagick
   - php5-imap
   - php5-interbase
   - php5-mcrypt
   - php5-memcache
   - php5-mysql
   - php5-pgsql
   - php5-pspell
   - php5-recode
   - php5-tidy
   - php5-xmlrpc
   - php5-xsl
  tags: httpd

- name: Habilitar módulo rewrite do Apache
  apache2_module: name=rewrite state=present
  notify: restart webserver

- name: http service state
  service: name=httpd state=started enabled=yes
  tags: httpd

- name: Criar o Virtualhost
  template: src=havaianas.conf dest=/etc/apache2/sites-available/havaianas

- name: Criar o diretório {{ path_projeto }}
  file: path={{ path_projeto }} state=directory

- name: Habilitar o VirtualHost {{ nome_projeto }}
  command: a2ensite {{ nome_projeto }}
  args:
    creates: /etc/apache2/sites-enabled/{{ nome_projeto }}.conf
  notify: restart webserver

main.yml

---
- include: install_httpd.yml
- include: web_utils.yml
- include: conf_apache.yml
- include: composer_drush.yml

Ansible no container, fazendo a mágica acontecer.

Esse é talvez o passo mais simples de todo esse tutorial, porém ele é o maior responsável pela mágica. Ao criar um container seu, personalizado, normalmente utilizamos o Dockerfile para termos uma imagem que atenda o que precisamos. E é dentro do Dockerfile que a gente coloca o Ansible pra rodar. 

Como eu já havia dito, esse é o passo mais simples, especialmente se você já manja da criação de um Dockerfile. Você cria um Dockerfile baseando-se numa imagem de sua preferência, clona o Ansible, define algumas variáveis de ambiente, usa o comando ansible-playbook e ALAKAZAM!, sua imagem nascerá provisionada pelo Ansible. 

Claro, existe um pequeno parâmetro para usar no comando que faz toda a diferença. Você precisa passar o parâmetro -c com o valor local, indicando para o Ansible que aquele playbook não provisionará uma máquina através de SSH, mas sim aquela máquina em que ele está rodando.

No final, você deverá ter um Dockerfile parecido com esse:

FROM ubuntu:12.04  
MAINTAINER Time On Going <on-going@justdigital.com.br>

RUN apt-get -y update  
RUN apt-get install -y python-yaml python-jinja2 git ##Instala as dependências do Ansible

RUN git clone https://github.com/ansible/ansible.git /tmp/ansible  
WORKDIR /tmp/ansible  
RUN git submodule update --init --recursive ##Os módulos core do Ansible são agora submódulos Git, então precisamos cloná-los também.

ENV PATH $PATH:/tmp/ansible/bin ##Coloca a pasta com os executáveis do Ansible no PATH  
ENV ANSIBLE_LIBRARY /tmp/ansible/library ##Define a variável com as bibliotecas do Ansible  
ENV PYTHONPATH /tmp/ansible/lib:$PYTHON_PATH ##Defien a variável para as bibliotecas do Python

ADD ./ansible /ansible ##Insere a pasta com as roles, os playbooks, etc  
WORKDIR /ansible  
RUN ansible-playbook web-playbook.yml -c local ##Roda o Ansible, indicando qual playbook e a conexão a ser usada, neste caso a local

WORKDIR /var/www/havaianas

EXPOSE 80

CMD ["apache2ctl", "-D", "FOREGROUND"]  

Fim e referências

Enfim, seguindo todos esses passos, ao fazer o build baseado no seu Dockerfile, você terá como resultado uma imagem, pronta para ser transformada em um container, totalmente provisionada pelo Ansible. E você poderá usar os playbooks criados por você para provisionar outros containeres, VM's, máquinas reais e etc.

Algumas referências úteis, que podem ajudar bastante:

 
Intro to Playbooks - "Getting Started" do próprio Ansible de como criar os seus playbooks

Dockerfile Reference - Todos os comandos possíveis para o Dockerfile, com ampla explicação para cada um deles.