Dia 23: Ansible no Linux — Playbooks, Inventários e Idempotência

O Ansible é a ferramenta de automação de configuração mais adoptada em ambientes Linux modernos. Permite gerir centenas de servidores a partir de uma única máquina de controlo, sem instalar agentes nos alvos — usa apenas SSH. Neste dia 23 do curso, vamos cobrir a instalação, a estrutura de inventários, a escrita de playbooks em YAML, os módulos essenciais, o conceito fundamental de idempotência, variáveis com facts, templates Jinja2, roles, e a protecção de segredos com Ansible Vault.

Nota: O Ansible corre numa máquina de controlo (o teu portátil ou um servidor de gestão) e envia comandos via SSH para os hosts geridos. Os hosts geridos só precisam de Python 3 e uma conta SSH — não há agentes, daemons nem portas adicionais para abrir.

Neste artigo

O que é o Ansible e por que usar

O Ansible é uma ferramenta de automação IT open-source, desenvolvida originalmente por Michael DeHaan e adquirida pela Red Hat em 2015. Diferentemente de ferramentas como Chef ou Puppet, o Ansible é agentless — não precisa de software instalado nos servidores alvo. Toda a comunicação acontece via SSH (ou WinRM para Windows), usando Python no lado remoto para executar os módulos.

As principais vantagens do Ansible para administradores Linux são:

  • Sem agentes: SSH já está disponível em praticamente todos os servidores Linux — não há nada para instalar nos hosts geridos.
  • Declarativo: descreves o estado desejado (ex: “o nginx deve estar instalado e a correr”) e o Ansible garante que esse estado se concretiza.
  • Idempotente: executar o mesmo playbook 10 vezes produz o mesmo resultado que executá-lo 1 vez — não duplica nem corrói configuração.
  • YAML legível: os playbooks são ficheiros de texto em YAML, fáceis de ler, escrever e revisar.
  • Curva de aprendizagem baixa: em poucas horas tens um playbook funcional a gerir dezenas de servidores.

O Ansible é usado para provisioning de servidores, configuração de serviços, deployment de aplicações, orquestração de upgrades zero-downtime e até apply de patches de segurança em fleet inteiro. Para sysadmins que gerem ambientes heterogéneos (Ubuntu, Debian, RHEL, Rocky, Alma), o Ansible normaliza a gestão via módulos que detectam a distribuição automaticamente (documentação do módulo package).

Instalação do Ansible

O Ansible instala-se apenas na máquina de controlo — o computador de onde vais correr os playbooks. Os hosts geridos não precisam de instalação. A forma mais simples é via gestor de pacotes da distribuição.

Em Ubuntu/Debian, o pacote ansible está nos repositórios oficiais. Em RHEL/Rocky/Alma, o Ansible vem via EPEL ou via pip. A versão mais recente e recomendada instala-se via pip (Python package manager), garantindo acesso à última versão estável independente da distribuição.

# Ubuntu / Debian
sudo apt update
sudo apt install -y ansible

# RHEL / Rocky / AlmaLinux
sudo dnf install -y epel-release
sudo dnf install -y ansible

# Alternativa universal via pip (recomendado para versão mais recente)
python3 -m pip install --user ansible

Depois de instalar, confirma a versão e que o binário está acessível:

ansible --version
# Esperado: ansible [core 2.16.x] ou superior, config file = /etc/ansible/ansible.cfg

O --version mostra a versão do Ansible, o ficheiro de configuração activo, o caminho do inventário por omissão e a localização dos módulos. Guarda esta informação — precisas de saber qual ficheiro de config o Ansible está a ler quando algo não funciona como esperado (ver referência de configuração oficial).

Se o pip não estiver instalado, instala-o primeiro:

# Ubuntu / Debian
sudo apt install -y python3-pip

# RHEL / Rocky / AlmaLinux
sudo dnf install -y python3-pip

Inventários — definindo os hosts

O inventário é o ficheiro onde defines quais servidores o Ansible vai gerir. Pode ser em formato INI (ficheiro hosts.ini) ou YAML. O formato INI é mais comum para inventários simples e é o que vamos usar aqui. O ficheiro por omissão fica em /etc/ansible/hosts, mas podes especificar um caminho personalizado com a flag -i (ver guia de inventários oficial).

Cria um ficheiro hosts.ini num directório de trabalho:

# hosts.ini — inventário simples
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11

[dbservers]
db1.example.com ansible_host=192.168.1.20

[all:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/id_ed25519

Cada secção entre parêntesis rectos é um grupo. Os hosts dentro de cada grupo herdam as variáveis definidas em [grupo:vars]. A secção [all:vars] aplica variáveis a todos os hosts. Os parâmetros mais usados por host são:

  • ansible_host — endereço IP ou hostname real (se diferente do nome declarado).
  • ansible_user — utilizador SSH para ligar ao host.
  • ansible_ssh_private_key_file — caminho da chave privada SSH.
  • ansible_port — porta SSH (útil se não for a 22 padrão).
  • ansible_becometrue para escalar privilégios via sudo.

Para verificar que o Ansible consegue contactar todos os hosts do inventário, corre um ping ad-hoc:

ansible all -i hosts.ini -m ping
# Esperado: cada host responde "pong" com cor verde se a ligação SSH funcionar

O -m ping chama o módulo ping — não é um ping ICMP, é um teste que verifica que o Python remoto está funcional e que o Ansible consegue executar código no host. Se um host responder com vermelho ou “unreachable”, verifica a conectividade SSH manualmente com ssh [email protected] antes de avançar.

Também podes correr comandos arbitrários via módulo shell ou command sem escrever um playbook:

ansible webservers -i hosts.ini -m command -a "uptime"
# Mostra o uptime de todos os hosts do grupo webservers

ansible dbservers -i hosts.ini -m shell -a "free -h"
# Mostra memória dos hosts do grupo dbservers

A diferença entre command e shell: o command não interpreta variáveis de shell nem pipes ($HOME, |, >); o shell passa o comando através de /bin/sh e interpreta tudo. Prefere command sempre que possível — é mais seguro e previsível.

Playbooks — estrutura e YAML

Um playbook é um ficheiro YAML que descreve uma sequência de tarefas (tasks) a executar num conjunto de hosts. Cada playbook contém pelo menos uma play — que associa um conjunto de hosts a uma lista de tarefas. O YAML é sensível a indentação: usa sempre espaços (nunca tabs) e mantém o mesmo número de espaços por nível (ver introdução a playbooks oficial).

Cria o teu primeiro playbook — instala e inicia o nginx num grupo de webservers:

# playbook.yml — instalar e iniciar nginx
---
- name: Instalar e configurar nginx nos webservers
  hosts: webservers
  become: true
  tasks:
    - name: Instalar nginx (Debian/Ubuntu)
      apt:
        name: nginx
        state: present
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Instalar nginx (RHEL/Rocky/Alma)
      dnf:
        name: nginx
        state: present
      when: ansible_os_family == "RedHat"

    - name: Garantir que o nginx está a correr
      service:
        name: nginx
        state: started
        enabled: true

    - name: Abrir porta 80 na firewall (Debian)
      ufw:
        rule: allow
        port: "80"
        proto: tcp
      when: ansible_os_family == "Debian"

    - name: Abrir porta 80 na firewall (RHEL)
      firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: true
      when: ansible_os_family == "RedHat"

Executa o playbook com:

ansible-playbook -i hosts.ini playbook.yml
# Esperado: cada task mostra "changed" (1ª execução) ou "ok" (execuções subsequentes)

Analisando a estrutura do playbook:

  • --- — separador de documento YAML, marca o início do ficheiro.
  • name — nome descritivo da play (aparece no output da execução).
  • hosts: webservers — indica o grupo do inventário onde as tasks vão correr.
  • become: true — escala privilégios com sudo nas tasks (equivalente a --become na linha de comando).
  • tasks: — lista de tarefas a executar, por ordem.
  • when: — condição que controla se a task executa ou é saltada (skipped).

A variável ansible_os_family é um fact — informação que o Ansible recolhe automaticamente sobre cada host antes de executar as tasks. Permite escrever playbooks que se adaptam à distribuição sem precisar de inventários separados.

Módulos Essenciais

Os módulos são as unidades de execução do Ansible — cada task chama um módulo. Existem centenas de módulos no ansible.builtin (incluídos por omissão) e milhares em coleções externas. Os mais usados para administração Linux são:

Módulo Função Exemplo de uso
apt Gestão de pacotes em Debian/Ubuntu Instalar, remover ou actualizar pacotes .deb
dnf Gestão de pacotes em RHEL/Rocky/Alma Instalar, remover ou actualizar pacotes .rpm
package Gestão de pacotes universal Detecta automaticamente apt/dnf/yum
service Gestão de serviços systemd Iniciar, parar, reiniciar, activar ao boot
file Gestão de ficheiros e directórios Criar, remover, mudar permissões
copy Copiar ficheiros da máquina de controlo para os hosts Deploy de ficheiros de configuração
template Gerar ficheiros a partir de templates Jinja2 Configuração dinâmica com variáveis
user Gestão de utilizadores Criar, remover, modificar contas
group Gestão de grupos Criar, remover, modificar grupos
lineinfile Editar uma linha específica num ficheiro Comentar/descomentar configurações
systemd Gestão avançada de unidades systemd Daemon-reload, restart, masking

Exemplo prático com vários módulos — criar um utilizador, copiar um ficheiro e garantir uma directoria:

# modulos.yml — exemplos de módulos essenciais
---
- name: Configurar ambiente de deploy
  hosts: webservers
  become: true
  tasks:
    - name: Criar grupo de aplicação
      group:
        name: deploy
        state: present

    - name: Criar utilizador de deploy
      user:
        name: deploy
        group: deploy
        shell: /bin/bash
        home: /home/deploy
        create_home: true

    - name: Criar directoria de aplicação
      file:
        path: /opt/app
        state: directory
        owner: deploy
        group: deploy
        mode: "0755"

    - name: Copiar ficheiro de configuração
      copy:
        src: files/app.conf
        dest: /opt/app/app.conf
        owner: deploy
        group: deploy
        mode: "0644"
        backup: true

    - name: Garantir que a linha de log está presente
      lineinfile:
        path: /opt/app/app.conf
        regexp: "^log_level="
        line: "log_level=info"
        state: present

O parâmetro backup: true no módulo copy cria uma cópia do ficheiro original com timestamp antes de o substituir — útil para rollback rápido se a nova configuração tiver problemas.

Idempotência — o coração do Ansible

A idempotência é o conceito mais importante do Ansible. Uma operação é idempotente quando executá-la uma ou múltiplas vezes produz exactamente o mesmo resultado — não há efeitos acumulativos nem laterais. Em termos práticos: se correres um playbook 10 vezes seguidas, o servidor fica no mesmo estado que se o tivesses corrido 1 vez.

Os módulos do Ansible são desenhados para ser idempotentes por omissão. Por exemplo, o módulo apt com state: present verifica se o pacote já está instalado antes de tentar instalá-lo. Se já estiver, a task marca como ok (verde) em vez de changed (amarelo) — não reinstala.

Atenção: os módulos shell e command NÃO são idempotentes por omissão. Se usares shell: echo "x" >> /etc/ficheiro, cada execução acrescenta uma linha nova. Para tornar estes módulos idempotentes, usa creates: ou changed_when:.

Exemplo de task não idempotente vs idempotente:

# NAO IDEMPOTENTE — acrescenta linha a cada execução
- name: Adicionar entrada ao hosts (errado)
  shell: echo "10.0.0.5 app.local" >> /etc/hosts

# IDEMPOTENTE — usa lineinfile com regexp
- name: Garantir entrada no hosts (correcto)
  lineinfile:
    path: /etc/hosts
    regexp: "^10\\.0\\.0\\.5\\s+app\\.local"
    line: "10.0.0.5 app.local"
    state: present

# IDEMPOTENTE com shell — só executa se o ficheiro nao existir
- name: Download de script de setup
  shell: curl -sL https://exemplo.com/setup.sh -o /opt/setup.sh
  args:
    creates: /opt/setup.sh

O parâmetro creates: diz ao Ansible: “só executa este comando se o ficheiro /opt/setup.sh não existir”. Se já existir, a task é saltada — o resultado é idempotente.

Outro padrão útil é o changed_when: para controlar quando uma task marca como “changed”:

- name: Verificar se reboot é necessário
  shell: needs-restarting -r 2>/dev/null; echo $?
  register: reboot_check
  changed_when: false  # task de leitura — nunca altera o sistema

- name: Reboot se necessário
  reboot:
  when: reboot_check.stdout_lines[-1] != "0"

Variáveis, Facts e Templates Jinja2

As variáveis no Ansible podem ser definidas em vários níveis: no inventário, em ficheiros group_vars/ e host_vars/, no próprio playbook, ou via --extra-vars na linha de comando. A precedência é: linha de comando > playbook > host_vars > group_vars > inventário.

Cria a estrutura de directórios para variáveis:

project/
  hosts.ini
  playbook.yml
  group_vars/
    webservers.yml
    dbservers.yml
  host_vars/
    web1.example.com.yml
  templates/
    nginx.conf.j2
  files/
    app.conf

Exemplo de group_vars/webservers.yml:

# group_vars/webservers.yml
nginx_worker_processes: auto
nginx_max_clients: 1024
nginx_listen_port: 80
app_name: "meu-app"

Os facts são informações que o Ansible recolhe automaticamente sobre cada host no início de cada play: sistema operativo, versão, arquitectura, endereços IP, discos, memória, CPU. Para ver todos os facts de um host:

ansible web1.example.com -i hosts.ini -m setup
# Mostra JSON com centenas de facts: ansible_os_family, ansible_distribution,
# ansible_architecture, ansible_default_ipv4, ansible_memtotal_mb, etc.

Os templates Jinja2 permitem gerar ficheiros de configuração dinâmicos combinando variáveis e facts. Cria um template de configuração nginx:

# templates/nginx.conf.j2
worker_processes {{ nginx_worker_processes }};
events {
    worker_connections {{ nginx_max_clients }};
}
http {
    server {
        listen {{ nginx_listen_port }};
        server_name {{ ansible_hostname }}.example.com;
        root /var/www/{{ app_name }};
        location / {
            try_files $uri $uri/ =404;
        }
    }
}

Usa o módulo template para gerar o ficheiro final no host remoto:

- name: Gerar configuração do nginx
  template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: nginx -t -c %s
  notify: reiniciar nginx

# Handlers — só executam quando uma task faz "notify"
handlers:
  - name: reiniciar nginx
    service:
      name: nginx
      state: restarted

O validate: nginx -t -c %s testa a sintaxe do ficheiro gerado antes de o aplicar — se a configuração for inválida, a task falha e o ficheiro original não é substituído. O notify acciona o handler “reiniciar nginx”, mas só se a task marcar como changed. Os handlers executam no final da play, e apenas uma vez, mesmo se várias tasks notificarem o mesmo handler.

Roles e ansible-galaxy

Quando os playbooks crescem além de 100-200 linhas, torna-se difícil mantê-los num único ficheiro. As roles são a estrutura de organização do Ansible — dividem a configuração em directórios padronizados com responsabilidades separadas (ver guia de roles oficial).

Cria uma role com ansible-galaxy:

ansible-galaxy init roles/nginx
# Cria a estrutura de directorios padrao:
# roles/nginx/
#   tasks/main.yml      — tarefas principais
#   handlers/main.yml   — handlers (restarts, reloads)
#   templates/          — templates Jinja2
#   files/              — ficheiros para copiar
#   vars/main.yml       — variaveis da role
#   defaults/main.yml   — variaveis com defaults
#   meta/main.yml       — metadata e dependencias

Exemplo de roles/nginx/tasks/main.yml:

# roles/nginx/tasks/main.yml
---
- name: Instalar nginx (Debian)
  apt:
    name: nginx
    state: present
    update_cache: true
  when: ansible_os_family == "Debian"

- name: Instalar nginx (RHEL)
  dnf:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

- name: Gerar configuração
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: nginx -t -c %s
  notify: reiniciar nginx

- name: Garantir nginx a correr
  service:
    name: nginx
    state: started
    enabled: true

Usa a role num playbook — fica limpo e reutilizável:

# site.yml — playbook principal que chama roles
---
- name: Configurar webservers
  hosts: webservers
  become: true
  roles:
    - nginx
    - firewall

O ansible-galaxy também serve para instalar roles partilhadas pela comunidade a partir do Ansible Galaxy:

# Instalar uma role do Galaxy
ansible-galaxy install geerlingguy.nginx

# Listar roles instaladas
ansible-galaxy list

# Instalar varias roles via requirements.yml
ansible-galaxy install -r requirements.yml
Atenção: roles do Ansible Galaxy são contribuições da comunidade — nem todas são mantidas nem seguras. Antes de usar uma role de terceiros em produção, revê o código fonte, verifica a última data de actualização e o número de downloads. Para ambientes críticos, prefere escrever as tuas próprias roles ou auditar as existentes.

Ansible Vault — segredos encriptados

Os playbooks frequentemente precisam de conter informação sensível: palavras-passe de bases de dados, chaves de API, tokens de acesso. O Ansible Vault permite encriptar ficheiros ou variáveis individuais para que os segredos não fiquem em texto claro no repositório (ver documentação do ansible-vault).

Encripta um ficheiro de variáveis sensíveis:

# Criar ficheiro encriptado novo
ansible-vault create group_vars/webservers/vault.yml
# Pede uma password — o ficheiro fica encriptado com AES-256

# Encriptar ficheiro existente
ansible-vault encrypt group_vars/webservers/secrets.yml

# Ver conteudo encriptado (pede password)
ansible-vault view group_vars/webservers/vault.yml

# Editar ficheiro encriptado
ansible-vault edit group_vars/webservers/vault.yml

Para executar um playbook que usa ficheiros encriptados, fornece a password do vault:

# Pede password interactivamente
ansible-playbook -i hosts.ini site.yml --ask-vault-pass

# Usa ficheiro de password (para automatizacao)
echo "minha_password_secreta" > .vault_pass
chmod 600 .vault_pass
ansible-playbook -i hosts.ini site.yml --vault-password-file .vault_pass
Nota: o ficheiro .vault_pass deve ter permissões 600 e NUNCA ser commitado para controlo de versões. Adiciona .vault_pass ao .gitignore. Para CI/CD, usa variáveis de ambiente ou um gestor de segredos externo (HashiCorp Vault, AWS Secrets Manager).

Erros Comuns

Problema Causa Solução
SSH connection refused / unreachable Host inacessível, porta SSH errada ou firewall a bloquear Verifica com ssh user@host manualmente; confirma ansible_port no inventário
“sudo: a password is required” O utilizador remoto precisa de password para sudo Configura NOPASSWD no sudoers ou usa --ask-become-pass
YAML syntax error Indentação com tabs em vez de espaços ou indentação inconsistente Usa espaços (nunca tabs); valida com ansible-playbook --syntax-check playbook.yml
“fatal: MODULE FAILURE — No module named ‘apt'” Python 3 não instalado ou não encontrado no host remoto Instala python3 no host; define ansible_python_interpreter=/usr/bin/python3
Task sempre “changed” mesmo sem alterações Módulo shell/command não idempotente Substitui por módulo específico ou adiciona changed_when: false / creates:
Variável não definida (undefined) Variável referenciada no template mas não declarada Define em defaults/main.yml ou usa {{ var | default('valor') }}
Handler não executa após notify Nome do notify não corresponde ao nome do handler Verifica que o nome em notify: é exactamente igual ao name: do handler
Vault password prompt em CI/CD --ask-vault-pass não funciona em pipelines não-interactivos Usa --vault-password-file com path de variável de ambiente

Checklist de Verificação

Antes de aplicar um playbook Ansible em produção, confirma cada ponto:

  1. Verifica a sintaxe YAML: corre ansible-playbook --syntax-check playbook.yml antes de executar.
  2. Faz um dry-run: usa --check para simular a execução sem alterar nada (ansible-playbook --check site.yml).
  3. Testa num host de staging: limita o playbook a um host de teste antes de aplicar em todo o fleet (--limit staging-host).
  4. Verifica a idempotência: executa o playbook duas vezes seguidas — a segunda execução não deve ter nenhuma task “changed”.
  5. Confirma as chaves SSH: testa ssh user@host manualmente para cada host novo no inventário.
  6. Encripta segredos: garante que nenhuma palavra-passe ou chave está em texto claro — usa ansible-vault encrypt em todos os ficheiros com informação sensível.
  7. Define become: true apenas onde necessário: não escalas privilégios em tasks que não precisam — minimiza a superfície de risco.
  8. Versiona tudo em Git: playbooks, inventários, roles e templates devem estar em controlo de versões para auditoria e rollback.

Artigos Relacionados