Primeiros passos com Ansible

Mascote LinuxPro aperta o botão de play com o playbook aberto na mão, e feixes de luz levam a configuração a três servidores, sob o logo do Ansible, com o cachorro caramelo cyborg sentado ao lado

Se você ainda configura servidor na mão, entrando por SSH, editando arquivo, instalando pacote e repetindo tudo na máquina seguinte, o Ansible é a ferramenta que aposenta esse ritual. Você descreve em arquivos YAML como cada servidor deve ficar, e o Ansible aplica isso por SSH, sem instalar agente nenhum nas máquinas. Pode rodar mil vezes: o resultado é o mesmo. Neste guia você vai instalar o Ansible no Ubuntu, montar um projeto com inventário, rodar comandos avulsos e escrever um playbook com variável, template e handler. Todos os comandos foram testados com o ansible-core 2.21 num laboratório com três servidores Ubuntu 24.04.

Como nasceu o Ansible

O primeiro commit do Ansible, com a mensagem “Genesis.”, é de 23 de fevereiro de 2012, feito por Michael DeHaan. Ele conhecia o problema de perto: na Red Hat, tinha criado o Cobbler, de provisionamento de servidores, e participado do Func, de execução remota de comandos. As ferramentas de gerência da época, como Puppet e Chef, exigiam um agente instalado em cada servidor e uma linguagem própria. O Ansible nasceu com três princípios que valem até hoje: sem agente (só SSH e Python, que já existem em qualquer Linux), YAML legível em vez de linguagem de programação e curva de aprendizado curta.

O nome vem da ficção científica. Ansível é o comunicador instantâneo entre planetas que Ursula K. Le Guin inventou em 1966, depois popularizado por O Jogo do Exterminador, de Orson Scott Card. A metáfora combina: comandar máquinas distantes na hora, sem intermediários.

O projeto virou empresa, a AnsibleWorks, depois Ansible Inc., e foi comprado pela Red Hat em outubro de 2015, que o mantém open source. Hoje o motor fica no pacote ansible-core, e o pacote ansible junta o motor com dezenas de collections da comunidade, os conjuntos de módulos para cada tecnologia.

Como o Ansible trabalha

O Ansible é instalado numa única máquina, a máquina de controle: o seu notebook ou um servidor de administração. Os servidores gerenciados só precisam de SSH e Python 3, que o Ubuntu e o Debian já trazem. Nada de agente, nada de serviço extra rodando neles.

Diagrama: a máquina de controle, com ansible.cfg, inventário, playbook, group_vars e roles, se conecta por SSH na porta 22 aos servidores web01, web02 e db01, que só precisam de sshd e Python 3

Para cada tarefa, o Ansible abre SSH nos servidores em paralelo (cinco por vez, por padrão), envia um pequeno programa Python, o módulo, executa, recebe o resultado em JSON e apaga o arquivo temporário. O módulo compara o estado atual com o que você pediu e só mexe se for preciso.

Diagrama do ciclo de uma tarefa: lê o inventário e a tarefa, conecta por SSH, envia o módulo Python, compara o estado atual com o desejado e responde em JSON com ok, changed ou failed; handlers rodam uma vez no fim do play

Instalando no Ubuntu

Há três caminhos, e a diferença é a versão:

  • Repositório do Ubuntu: o mais simples. No Ubuntu 24.04 ele traz o pacote ansible 9.2, com ansible-core 2.16. Serve para aprender.
  • PPA oficial do projeto: versão atual. Em setembro de 2026, o PPA entrega o ansible 14.4.0, com ansible-core 2.21.4, para o Ubuntu 24.04 e o 26.04. Foi o que usamos nos testes.
  • pipx: isola o Ansible do Python do sistema e funciona em qualquer distribuição.
# Opção 1: repositório do Ubuntu
sudo apt update
sudo apt install -y ansible

# Opção 2: PPA oficial (versão atual)
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

# Opção 3: pipx
sudo apt install -y pipx
pipx install --include-deps ansible
pipx ensurepath   # abra um novo terminal depois deste comando
ansible --version
ansible [core 2.21.4]
  config file = /etc/ansible/ansible.cfg
  ...
  python version = 3.12.3 (main, Aug 31 2026, 10:18:26) [GCC 13.3.0] (/usr/bin/python3)
  jinja version = 3.1.2

A estrutura de um projeto

O Ansible roda de qualquer diretório, mas vale adotar a estrutura convencional desde o primeiro dia:

meu-projeto/
├── ansible.cfg            # configuração do projeto
├── inventory.ini          # quais máquinas gerenciar
├── site.yml               # playbook principal
├── group_vars/            # variáveis por grupo de hosts
│   └── webservers.yml
├── host_vars/             # variáveis de um host específico
│   └── web01.yml
└── roles/                 # partes reutilizáveis
    └── nginx/
        ├── tasks/main.yml
        ├── handlers/main.yml
        ├── templates/
        ├── files/
        └── defaults/main.yml
  • ansible.cfg: configurações do projeto. O Ansible usa o arquivo do diretório atual antes do global, em /etc/ansible/ansible.cfg.
  • Inventário: a lista de máquinas, organizada em grupos.
  • Playbook: o roteiro em YAML, “nestes hosts, garanta estas tarefas”.
  • group_vars e host_vars: valores que mudam por grupo ou por máquina, separados da lógica.
  • Roles: playbooks empacotados para reaproveitar em outros projetos.

Inventário, chave SSH e o primeiro ping

Crie o projeto com três servidores em dois grupos. Troque os IPs pelos seus:

mkdir meu-projeto && cd meu-projeto

cat > inventory.ini <<'EOF'
[webservers]
web01 ansible_host=192.168.64.2
web02 ansible_host=192.168.64.3

[dbservers]
db01 ansible_host=192.168.64.4

[all:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3
EOF

cat > ansible.cfg <<'EOF'
[defaults]
inventory = inventory.ini
EOF

ansible-inventory --graph
@all:
  |--@ungrouped:
  |--@webservers:
  |  |--web01
  |  |--web02
  |--@dbservers:
  |  |--db01

A linha ansible_python_interpreter fixa o Python usado nos servidores. Sem ela, o ansible-core 2.21 funciona, mas imprime um aviso em cada host dizendo que usou o Python “descoberto” e que isso pode mudar se outro Python for instalado.

O Ansible entra nos servidores com a sua chave SSH. Se você ainda não tem uma, gere e copie para cada servidor:

ssh-keygen -t ed25519
for ip in 192.168.64.2 192.168.64.3 192.168.64.4; do ssh-copy-id ubuntu@$ip; done

O ssh-copy-id também pede para você confirmar a chave de cada servidor na primeira conexão. Se você usar outro caminho para distribuir a chave, o Ansible vai parar com Host key verification failed, e é bom que pare: é a proteção contra servidor falso. Para registrar as chaves de uma vez, sem desligar a checagem:

ssh-keyscan -H 192.168.64.2 192.168.64.3 192.168.64.4 >> ~/.ssh/known_hosts

Confira as chaves se o servidor não for de laboratório. Muito tutorial manda colocar host_key_checking = False no ansible.cfg; evite em produção. E se colocar qualquer opção, cuidado com comentários: no ansible.cfg, o # só vale no começo da linha. No nosso teste, a linha host_key_checking = True # comentário foi lida como False, porque o Ansible pegou o texto inteiro como valor, não reconheceu como verdadeiro e desligou a checagem sem avisar. Comentário na mesma linha só com ;, ou numa linha própria.

Agora o teste de comunicação:

ansible all -m ansible.builtin.ping
web01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web02 | SUCCESS => { ... }
db01 | SUCCESS => { ... }

O módulo ping não é o ping de rede (ICMP): ele confirma que o Ansible entra por SSH e executa Python do outro lado, que é o que importa. Sem servidor à mão? Tudo funciona contra a própria máquina com ansible localhost -m ansible.builtin.ping, que usa conexão local.

Comandos ad-hoc

Antes de escrever playbooks, dá para resolver muita coisa com comandos avulsos. -m escolhe o módulo, -a passa os argumentos e --become executa com sudo do outro lado:

ansible all -a "uptime -p"                                     # comando em todos
ansible webservers -m ansible.builtin.apt \
  -a "name=htop state=present update_cache=true" --become      # instala pacote
ansible db01 -m ansible.builtin.setup -a "filter=ansible_distribution*"   # fatos
ansible-doc ansible.builtin.apt                                # documentação do módulo

Rode o comando do htop duas vezes. Na primeira, os hosts aparecem como CHANGED; na segunda, como SUCCESS, porque o pacote já estava lá. Se o seu usuário pede senha no sudo, acrescente -K (--ask-become-pass) para o Ansible perguntar. E prefira os módulos específicos ao shell: o apt sabe dizer se mudou algo, e um comando solto não sabe.

O primeiro playbook

Agora um roteiro completo: instalar o Nginx nos servidores web, publicar uma página gerada a partir de um template com variável, manter o serviço ativo e recarregá-lo só quando a página mudar. Primeiro a variável do grupo, em group_vars/webservers.yml:

---
site_titulo: "Servidor gerenciado pelo Ansible"

O template, em templates/index.html.j2, usa a sintaxe Jinja2. inventory_hostname e ansible_facts são preenchidos pelo próprio Ansible:

<!doctype html>
<html lang="pt-br">
<head><meta charset="utf-8"><title>{{ site_titulo }}</title></head>
<body>
  <h1>{{ site_titulo }}</h1>
  <p>Host: {{ inventory_hostname }} ({{ ansible_facts["distribution"] }} {{ ansible_facts["distribution_version"] }})</p>
</body>
</html>

E o playbook, site.yml:

---
- name: Configurar servidores web
  hosts: webservers
  become: true

  tasks:
    - name: Instalar o Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Publicar a página inicial
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: /var/www/html/index.html
        owner: root
        group: root
        mode: "0644"
      notify: Recarregar o Nginx

    - name: Garantir o Nginx ativo e habilitado no boot
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Recarregar o Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Os nomes dos módulos aparecem completos, como ansible.builtin.apt. É o formato recomendado desde o Ansible 2.10: deixa claro de qual collection vem cada módulo. O cache_valid_time evita rodar apt update de novo se ele foi feito na última hora.

ansible-playbook site.yml --syntax-check
ansible-playbook site.yml
TASK [Instalar o Nginx] ********************************************************
changed: [web01]
changed: [web02]

TASK [Publicar a página inicial] ***********************************************
changed: [web01]
changed: [web02]

TASK [Garantir o Nginx ativo e habilitado no boot] *****************************
changed: [web02]
changed: [web01]

RUNNING HANDLER [Recarregar o Nginx] *******************************************
changed: [web02]
changed: [web01]

PLAY RECAP *********************************************************************
web01                      : ok=5    changed=4    unreachable=0    failed=0    skipped=0 ...
web02                      : ok=5    changed=4    unreachable=0    failed=0    skipped=0 ...

Idempotência: rode de novo

Execute o mesmo comando outra vez e olhe o resumo:

web01                      : ok=4    changed=0    unreachable=0    failed=0 ...
web02                      : ok=4    changed=0    unreachable=0    failed=0 ...

changed=0. Essa é a idempotência, o coração do Ansible: ele não “executa comandos”, ele garante estados. O Nginx já estava instalado, a página já era a certa, o serviço já estava ativo, e nada foi tocado. O handler também não rodou, porque nenhuma tarefa mudou.

Simular antes de aplicar: --check e --diff

Mude o título em group_vars/webservers.yml para “Servidor web do LinuxPro” e simule, só no web01:

ansible-playbook site.yml --check --diff --limit web01
TASK [Publicar a página inicial] ***********************************************
--- before: /var/www/html/index.html
+++ after: /root/.ansible/tmp/ansible-local-586tfwktpl6/tmp5owga2ot/index.html.j2
@@ -1,8 +1,8 @@
 <!doctype html>
 <html lang="pt-br">
-<head><meta charset="utf-8"><title>Servidor gerenciado pelo Ansible</title></head>
+<head><meta charset="utf-8"><title>Servidor web do LinuxPro</title></head>
 <body>
-  <h1>Servidor gerenciado pelo Ansible</h1>
+  <h1>Servidor web do LinuxPro</h1>
   <p>Host: web01 (Ubuntu 24.04)</p>
 </body>
 </html>

changed: [web01]

O --check não altera nada, e o --diff mostra exatamente o que mudaria. Aplicando de verdade, só a página e o handler aparecem como changed, e o Nginx é recarregado uma única vez.

Dois limites do --check, vistos nos testes:

  • Num servidor limpo, ele pode falhar no meio. Na simulação, o Nginx não é instalado de verdade, então a tarefa seguinte, a do serviço, termina com Could not find the requested service nginx. É esperado: a simulação vale mais para revisar mudanças em servidores que já estão configurados.
  • O módulo apt precisa do pacote python3-apt para simular. O Ubuntu Server já o traz; imagens mínimas, como as de contêiner, não, e o erro diz python3-apt must be installed to use check mode. Numa execução normal, o próprio módulo instala o pacote.

Transformando o playbook em role

Quando o playbook cresce, as tarefas viram uma role, que pode ser usada em qualquer projeto:

ansible-galaxy role init roles/nginx

O comando cria a estrutura em roles/nginx/. Mova as tarefas para roles/nginx/tasks/main.yml, o handler para roles/nginx/handlers/main.yml e o template para roles/nginx/templates/. Dentro da role, o src do template passa a ser só index.html.j2, porque o Ansible procura na pasta templates/ da própria role. O site.yml fica curto:

---
- name: Configurar servidores web
  hosts: webservers
  become: true
  roles:
    - nginx

Rodando de novo, as tarefas aparecem com o prefixo da role (nginx : Instalar o Nginx) e o resultado continua changed=0: mudou a organização, não o estado dos servidores.

Próximos passos

Com esta base, os caminhos naturais são:

Instale, faça o ping, rode o playbook duas vezes e veja o changed=0: nesse momento fica claro por que tanta gente parou de configurar servidor na mão.