ansible localhost -a "/bin/echo Primeiros passos com o Ansible"

Postado em 12 July, 2022 | 13 minutos

Obs.: Originalmente publicado no computando-arte dia 12Jul2022

Lembra quando precisou configurar pela primeira vez seu computador? Possivelmente precisou instalar o git, docker, VSCode, um navegador decente, um shell bacana como o zsh, mó trampo né?

Hoje vamos falar de uma ferramenta de automação de configuração, o Ansible, que permite que a configuração e manutenção dos ambientes seja feita de forma automatizada, poupando bastante tempo.

logo do Ansible

logo do Ansible

O que é o Ansible? O Ansible é uma ferramenta de automação de configuração e manutenção de ambientes, e atua agilizando e escalando essas atividades.

As principais vantagens do Ansible são sua facilidade de aprendizado, e não ter a necessidade de instalar um agente da ferramenta nas máquinas gerenciadas. A desvantagem acaba sendo que como não existe um agente executando o Ansible este processo não escala bem, ou seja para gerenciar muitos computadores o nó de controle do Ansible precisa ser potente.

Instalando o Ansible

Para instalar o Ansible é necessária a instalação da linguagem python e então basta instalar o executável através do pip install

pip install ansible

Uma funcionalidade que gosto muito no python são os ambientes virtuais (venv), eles são nativos da linguagem e permitem que você possa ter um “ambiente python” diferente para cada projeto, e assim instalar as dependências adequadas de cada projeto sem interferir nos demais.

Normalmente o venv fica dentro da pasta do projeto, e é ignorado no .gitignore. Para criar o venv faça

python3 -m venv venv

Agora toda vez que for utilizar o venv ative com:

source venv/bin/activate

Obs: se estiver usando zsh ou outro shell coloque a extensão do seu shell (no zsh ficaria activate.zsh).

Repare que o prompt (PS1) ficou diferente, contendo o nome do venv (venv).

Para evitar problemas, a primeira vez que crio o venv atualizo o pip e outras ferramentas:

pip install -U pip setuptools wheel

Com venv criado basta só instalar o ansible normalmente (pip install ansible).

Bora testar a instalação? Rode o seguinte

ansible -m ping localhost

Se tiver a seguinte saída significa que deu bom:

[WARNING]: No inventory was parsed, only implicit localhost is available

localhost | SUCCESS => {

    "changed": false,

    "ping": "pong"

}

Bora para o hello world?

ansible localhost -a "/bin/echo ola mundo"
[WARNING]: No inventory was parsed, only implicit localhost is available

localhost | CHANGED | rc=0 >>

ola mundo

Como o Ansible funciona?

O Ansible é executado no nó de controle e conecta via o protocolo ssh nos nós gerenciados. Para executar a ferramenta recebe como entrada um arquivo de texto que é o inventário com informações dos nós gerenciados, como os IPs e credenciais. A outra entrada é o playbook, um arquivo no formato yaml, que contém as instruções a serem executadas.

Um requisito para o ansible é que os nós gerenciados precisam do python instalado, porém é possível rodar o ansible de forma limitada para instalar o python para contornar isso ;)

Fonte: twitter.com/scienceshitpost

Fonte: twitter.com/scienceshitpost

Montando o inventário

Antes de montar o inventário precisamos de pelo menos uma máquina para conectarmos via ssh, no meu caso utilizei uma raspberrypi e uma máquina virtual. Sem enrolação, vamos ao inventário:

inventario.ini:

[pies]
rpi4 ansible_host=192.168.1.205 ansible_user=piuser ansible_password=pipass ansible_become=yes

[vms]
rockylinux ansible_host=192.168.121.182 ansible_user=rockyuser ansible_password=rockypass

[vms:vars]
ansible_become=yes

[all:vars]
ansible_port=22
ansible_become_method=sudo
ansible_python_interpreter=python3
ansible_become_password="{{ ansible_password }}"

O inventário pode ser escrito no formato ini ou no formato yaml, aqui optei pelo ini.

Nesse exemplo foram definidas duas máquinas a rpi4 pertencente ao grupo pies e rockylinux pertencente ao grupo vms.

E essas variáveis?

  • ansible_host é o ip ou domínio da máquina.
  • ansible_port é porta usada pelo ssh, por padrão é a 22, ou seja nem precisaríamos mudar no nosso caso.
  • ansible_user e ansible_password usuário e senha do ssh utilizados para conectar.
  • ansible_become indica se deve elevar privilégios ou não, ou seja rodar como usuário root.
  • ansible_become_method indica o método utilizado para elevar privilégios, aqui estamos utilizando o sudo, mas poderia ser su e utilizar a senha do root.

O inventário pode ser montado de diversas formas e tem várias outras variáveis, consulte a documentação para saber mais: How to build your inventory. Uma coisa que gosto de fazer é utilizar a opção ansible_ssh_common_args com -F ssh_config, onde ssh_config (man page) é um arquivo de configuração do ssh.

As vezes não conseguimos acessar diretamente todas as máquinas, pois é exposto apenas uma máquina, chamada de bastião que atua como proxy para acessar as outras máquinas dentro da rede privada. No ssh_config podemos usar a opção ProxyJump.

Outra vantagem é que fica mais fácil de acessar manualmente uma máquina, basta fazer ssh -F ssh_config nome_maquina e pronto ;)

Bora testar o inventário? Rode

ansible -m ping -i inventario.ini all

Putz, deu ruim, to use the ‘ssh’ connection type with passwords or pkcs11_provider, you must install the sshpass program . Por padrão o ansible conecta autenticando com chaves ssh, não a senha, então precisamos instalar o sshpass. Bastou um sudo apt install sshpass , pra quem tem mac tem um pacote no brew (brew install hudochenkov/sshpass/sshpass).

Agora foi:

rockylinux | SUCCESS => {

    "changed": false,

    "ping": "pong"

}

rpi4 | SUCCESS => {

    "changed": false,

    "ping": "pong"

}

Rodando comandos adhoc

Até aqui só fizemos -m ping, mas podemos fazer praticamente qualquer coisa, no ansible as tarefas são executadas através de módulos e vamos mostrar alguns deles aqui.

Caso não seja especificado nenhum módulo o ansible executa por padrão o módulo command, vou executar o comando hostnamectl, que exibe diversas informações do computador:

ansible -i inventario.ini -a hostnamectl all
rockylinux | CHANGED | rc=0 >>

   Static hostname: localhost.localdomain

         Icon name: computer-vm

           Chassis: vm

        Machine ID: 3d137ab51d6c4a1098281d0f08170bce

           Boot ID: 95f4fb49069040b484410562dbbf344c

    Virtualization: kvm

  Operating System: Rocky Linux 8.6 (Green Obsidian)

       CPE OS Name: cpe:/o:rocky:rocky:8:GA

            Kernel: Linux 4.18.0-372.9.1.el8.x86_64

      Architecture: x86-64

rpi4 | CHANGED | rc=0 >>

   Static hostname: rpi4

         Icon name: computer

        Machine ID: 8e5eb04e49a74e26952554ad422b19fb

           Boot ID: 18563abe0fa6420fb55488b28401304e

  Operating System: Debian GNU/Linux 11 (bullseye)

            Kernel: Linux 5.10.0-15-arm64

      Architecture: arm64

Deu bom! Conseguimos visualizar as infos sobre as máquinas. Por curiosidade, qual é desse rockylinux? A redhat descontinuou o centos em dez/2020, que é uma versão gratuita e comunitária do redhat enterprise linux (RHEL) e com o fim do centos dois projetos surgiram como sucessores compatíveis ao centos, o rockylinux e o almalinux.

Bora instalar um pacote como o neofetch? Só mandar um sudo apt install neofetch, certo? No nosso caso uma máquina tem uma distro baseada em redhat ou seja não utiliza o gerenciador de pacotes apt, mas o dnf. Por isso vamos utilizar o módulo package que é genérico e funciona com diversos gerenciadores de pacotes.

xkcd #1654

xkcd #1654

Vendo a documentação do módulo package, temos 3 argumentos, dos quais apenas 2 são obrigatórios, o nome dos pacotes (name) e o estado (state) como instalado (present) ou removido (absent).

ansible -i inventario.ini -m package -a "name=neofetch state=present" all
rpi4 | CHANGED => {

    "cache_update_time": 1655557364,

    "cache_updated": false,

    "changed": true,

    "stderr": "",

    "stderr_lines": [],

    "stdout": "Reading package lists...\nBuilding dependency tree… [...]

Rodando o neofetch: ansible -i inventario.ini -a neofetch all

Uma opção que facilita muito é a --limit que como o nome sugere limita a execução a algumas máquinas, pode ser especificada múltiplas vezes. Até então utilizamos o all que executa em todas as máquinas. Você pode trocar o all por um grupo(s) ou máquina(s) . E também podemos limitar qual não queremos que sejam incluídas, utilizando o ! como operador de negação.

Por exemplo, queremos reiniciar todas as máquinas, excepto as que pertencem ao grupo vms:

ansible -i inventario.ini -m reboot all --limit !vms
rpi4 | CHANGED => {

    "changed": true,

    "elapsed": 68,

    "rebooted": true

}

Usando o ansible-vault para proteger segredos

Quando criamos o inventário, colocamos as credenciais de acesso, isso não é uma boa prática pois pode acabar expondo essas credenciais. O que podemos fazer nesses casos?

Uma opção simples é tirar as senhas do inventário e digitar apenas quando precisar, utilizando as opções –ask-pass (ou abreviando -k) e –ask-become-pass (abreviando -K).

Mas vamos focar na abordagem do ansible-vault, que funciona como um gerenciador de senhas, onde os segredos ficam armazenados em um arquivo que está protegido por uma única senha.

Para usar o ansible-vault vamos primeiro tirar as senhas do inventário:

inventario.ini:

[pies]
rpi4 ansible_host=192.168.1.205 ansible_user=piuser  ansible_password="{{ rpi4_pass }}"

[vms]
rockylinux ansible_host=192.168.121.182 ansible_user=rockyuser ansible_password="{{ rocky_vm_pass }}"

[all:vars]
ansible_become=yes
ansible_become_method=sudo
ansible_python_interpreter=python3
ansible_become_password="{{ ansible_password }}"

Repare que em ambas as máquinas a variável a ansible_password estão sendo recebidas como referência.

Crie um arquivo que vamos chamar de vault.yml

vault.yml

---
rpi4_pass: 'pipass'
rocky_vm_pass: 'rockypass'

Agora que a mágica acontece, vamos criptografar o arquivo: ansible-vault encrypt vault.yml e digite a senha desejada. Por curiosidade, bora ver o conteúdo do arquivo?

$ANSIBLE_VAULT;1.1;AES256
33303863323038363938333365326637343733313432626231623735666433303965333266383934
6332633534626465353436666539646466633633393362370a353161653663633263663635643466
[...]

Quando precisar editar o vault, basta fazer ansible-vault edit vault.yml

E para carregar os segredos definidos no vault vamos usar a opção –extra-vars (abreviada por -e) onde podemos fazer -e “var=value” mas vamos apontar para um arquivo então -e @vault.yml. E a opção –ask-vault-pass para entrar com a senha do vault

ansible -i inventario.ini -m ping all -e @vault.yml --ask-vault-pass
rockylinux | SUCCESS => {

    "changed": false,

    "ping": "pong"

}

rpi4 | SUCCESS => {

    "changed": false,

    "ping": "pong"

}

Preciso digitar a senha do vault toda vez? Não necessariamente, uma abordagem é colocar a senha em um arquivo local e utilizar a opção –vault-password-file , mas sou preguiçoso :P e vou deixar configurado em um arquivo de configuração, o ansible.cfg edai fica tudo setado lá 😊

ansible.cfg

[defaults]
host_key_checking = True
inventory = inventario.ini
vault_password_file = ../.arquivo_com_a_senha.txt

Documentação com todas opções do ansible.cfg: Ansible Configuration Settings

Outra abordagem é a conhecida pela buzzword GitOps, que consiste em rodar o ansible no próprio repositório onde ficam os playbooks do ansible, utilizando um sistema de integração contínua, como github actions que falamos anteriormente aqui. Dessa forma a senha do vault pode ser passada via variável de ambiente e colocada em temporariamente um arquivo ou via argumento do comando.

Escrevendo nosso primeiro playbook

Chegou a hora! Bora escrever nosso primeiro playbook. Para isso vamos subir um servidor web, o apache, para hospedar um site um site estático.

Quando fui instalar o apache no rockylinux deu ruim, porque o pacote no debian é o apache2 mas no rockylinux é httpd. Como faz agora? Como eu faço pro ansible detectar que a máquina é o rockylinux ou debian e tratar adequadamente cada caso?

A ferramenta roda automaticamente um módulo chamado setup que obtém diversas infos das máquinas, e disponibiliza em uma variável chamada ansible_facts.

Fazendo ansible -m setup -e @vault.yml all obtive:

rpi4 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.1.205"
        ],
        [...]
        "ansible_distribution": "Debian",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "11",
        "ansible_distribution_release": "bullseye",
        "ansible_distribution_version": "11",
        "ansible_os_family": "Debian",
        [...]
}

rockylinux | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.121.182"
        ],
        [...]
        "ansible_distribution": "Rocky",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/redhat-release",
        "ansible_distribution_file_variety": "RedHat",
        "ansible_distribution_major_version": "8",
        "ansible_distribution_release": "Green Obsidian",
        "ansible_distribution_version": "8.6",
        "ansible_os_family": "RedHat",
        [...]
}

Com isso, podemos usar a variável ansible_distribution (Debian e Rocky) ou ansible_os_family (Debian e RedHat), optei pela ansible_os_family para que nosso playbook seja mais genérico.

playbook.yml

---
- name: Configura um site estatico.
  hosts: all
  become: true

  vars:
    apache_name_dict:
      "Debian": apache2
      "RedHat": httpd

  handlers:
    - name: restart apache
      service:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: restarted

  tasks:
    - name: Instala o apache.
      package:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: present

    - name: Copia o html da pag.
      copy:
        src: page.html
        remote_src: no
        dest: "/var/www/html/index.html"
        mode: 0664
      notify: restart apache

    - name: Habilita o servico do apache.
      service:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: started
        enabled: true

O playbook funciona da seguinte forma:

  1. Primeiro é instalado o pacote do apache, usando uma variável para mapear o nome correto do pacote.
  2. Copia o html do nosso site, o html precisa estar localizado ao lado do playbook, mas caso o arquivo já estivesse no nó gerenciado basta trocar a opção remote_src para yes.
  3. Habita o serviço do apache para que o mesmo seja iniciado automaticamente quando a máquina é iniciada.

O que é esse notify na task de copiar o html? Pense no seguinte: depois de copiar o html preciso que o apache seja reiniciado para carregar o html da página que acabamos de enviar, mas e se o html que estiver lá não foi alterado? Dessa forma o Ansible só reinicia o apache caso necessário, chamando o handler previamente definido.

Bora escrever sem usar o handler, pra ficar mais claro?

playbook2.yml

---
- name: Configura um site estatico.
  hosts: all
  become: true

  vars:
    apache_name_dict:
      "Debian": apache2
      "RedHat": httpd

  tasks:
    - name: Instala o apache.
      package:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: present

    - name: Copia o html da pag.
      copy:
        src: page.html
        remote_src: no
        dest: "/var/www/html/index.html"
        mode: 0664
      register: html_copy

    - name: restart apache.
      service:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: restarted
      when: html_copy.changed

    - name: Habilita o servico do apache.
      service:
        name: "{{ apache_name_dict[ansible_os_family] }}"
        state: started
        enabled: true

Nessa versão do playbook, repare que na task de copiar o html estamos fazendo register: html_copy como o nome sugere estamos registrando o resultado daquela task em uma variável. E na task de reiniciar o apache é acionada apenas quando mudou, em when: html_copy.changed.

Para rodar o comando é sugestivo: ansible-playbook -e @vault.yml playbook.yml

Será que o site tá de pé? Abrindo o navegador no ip da VM:

Uma coisa que você precisa ficar de olho quando escreve seus playbooks é se eles são idempotentes, que significa que o efeito de rodar o playbook é o mesmo independente de quantas vezes o mesmo foi executado. Para quem curte linguagens funcionais tá ligado qualé: O playbook não pode ter efeitos colaterais.

Na prática, assim que rodar a primeira vez execute novamente e veja no final se teve alguma task como changed.

O principal desafio para tornar os playbooks idempotentes acontece quando as tasks são do tipo command ou shell, afinal como o ansible vai saber se a task foi executada anteriormente com sucesso?

Nesses casos, uma possibilidade é utilizar o parâmetro chamado creates, disponível nos módulos de command e shell, que aponta para um arquivo marcador que seu shell script ou command precisa criar quando executado com sucesso.

Bora ver na prática?

playbook3.yml

---
- hosts: all
  become: true

  tasks:
    - name: inicializa um arquivo
      shell: echo "passei por aqui" > /etc/marcador_xpto.txt
      args:
        creates: /etc/marcador_xpto.txt

Repare que quando executamos a primeira vez no recap a task foi changed, mas na segunda vez não houve mudanças.

Quem tiver interesse, tem um texto excelente que até saiu no hacker news com várias dicas de como escrever scripts bash idempotentes: How to write idempotent Bash scripts – Fatih Arslan

Conclusão

Uma regra de ouro é: Antes de sair automatizando, primeiro documente o processo manual, quais recursos sua aplicação precisa? Com os recursos disponíveis, como deve ser feita a configuração da aplicação? Dessa forma fica fácil de vislumbrar o todo e não precisa sair caçando nos códigos. Apesar do manifesto ágil dar preferência a ter um software funcional a documentação abrangente, sugiro não deixar de documentar no início essas definições.

Para aprender ansible, a melhor referência é o livro do Jeff Geerling, Ansible for DevOps o livro tem vários exemplos e dicas. Pra quem prefere o formato de vídeos, no início da pandemia o autor trouxe o conteúdo do livro em forma de lives: Ansible 101 playlist. E pra quem prefere ir direto nos códigos os exemplos estão disponíveis nesse github: github.com/geerlingguy/ansible-for-devops

Obrigado por acompanhar esse texto, espero que te ajude, caso tenha algum feedback ou dúvida não hesite em entrar em contato.