Como criar primeiro playbook Ansible no VPS
Aprenda a instalar Ansible via pipx no Ubuntu 24.04 e crie um playbook de hardening. Inclui correção de erros de Permission denied e sudo no inventory.
O que você está construindo
Uma máquina de controle com Ansible instalado e um ou mais VPS Ubuntu 24.04 recém-instalados apenas com a imagem padrão. Ao final, você terá um arquivo de inventory com o nome dos seus servidores, um ping ad-hoc que prova que a autenticação funciona de ponta a ponta, e um playbook que executa todo o checklist de um novo VPS como código: um usuário de deploy com sua chave SSH, sshd configurado com hardening, fail2ban, unattended upgrades e um firewall que permite OpenSSH antes de bloquear todo o resto. Aponte para um servidor ou vinte. Execute duas vezes e a segunda execução não alterará nada — esse é o objetivo principal.
Após quinze anos provisionando VPS, posso lhe dizer o padrão real: todos configuram os primeiros cinco servidores manualmente, depois perdem um fim de semana no sexto porque ninguém lembra o que foi feito nos cinco primeiros. Este guia aprofunda o conteúdo de gerenciamento de múltiplos servidores Linux — comece a lê-lo no dia em que você se pegar digitando o mesmo apt install em três terminais diferentes.
O que o Ansible realmente é, em um parágrafo
O Ansible é agentless. Não há necessidade de instalar um daemon nos servidores gerenciados: a máquina de controle conecta via SSH comum, copia um pequeno módulo Python para o destino, o executa, lê o JSON gerado e o deleta. O único requisito do destino é o python3, que já está presente em qualquer imagem padrão do Ubuntu. A palavra fundamental é idempotente, o que significa algo simples: uma tarefa descreve um estado, não uma ação. state: present para um pacote significa "garanta que este esteja instalado", e não "execute o instalador". Se o estado já for mantido, o Ansible não altera nada e reporta como ok em vez de changed. Essa propriedade é o diferencial do produto — é o que torna seguro reexecutar um playbook, e reexecuções seguras são o que transformam um shell script em infraestrutura.
Pré-requisitos e problemas comuns
- Uma máquina de controle: seu laptop ou um VPS pequeno. Consideramos o Ubuntu 24.04; o macOS funciona de forma idêntica após a instalação do pipx via Homebrew.
- Um ou mais VPSs de destino rodando Ubuntu 24.04 em KVM, acessíveis como root. Nada será instalado neles.
- Autenticação por chave SSH em cada destino. O Ansible utiliza a mesma autenticação do seu comando
ssh— se ossh root@hostsolicitar senha, o Ansible falhará. - No Ubuntu 24.04, o
pip install ansiblefalha comerror: externally-managed-environment. Isso é uma política deliberada da distro, não um erro. Use o pipx. - O espaçamento em YAML é sintaxe. Um recuo incorreto gera
mapping values are not allowed in this context, e um caractere de tabulação em qualquer lugar causará erro fatal. - Mantenha uma sessão SSH aberta em cada destino enquanto o playbook realiza o hardening do sshd. Todo lockout que ajudei um cliente a recuperar envolveu o fechamento da última sessão "para testar do zero".
Passo 1: instale o Ansible na máquina de controle com pipx, não com pip
O instinto comum é usar pip3 install ansible. Em uma imagem 24.04 totalmente limpa que falha um passo antes — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — instalar o pip apenas leva ao erro real:
pip3 install ansibleerror: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.O Ubuntu 24.04 marca o Python do sistema como gerenciado externamente (PEP 668), portanto o pip não pode conflitar com o apt pelos mesmos arquivos. Não tente usar --break-system-packages; o flag tem o nome correto. A solução limpa é o pipx, que cria um virtualenv isolado para o Ansible e adiciona os binários ao seu PATH:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleAbra um novo shell após pipx ensurepath para que a alteração no PATH seja aplicada. --include-deps não é apenas estética: o pacote ansible não possui scripts de console próprios — ansible, ansible-playbook e os demais são pontos de entrada da sua dependência ansible-core — portanto, sem o flag, o pipx recusa a instalação com No apps associated with package ansible or its dependencies. Instale o pacote ansible, e não o ansible-core puro — o pacote completo inclui as coleções da comunidade, e este playbook utiliza módulos de duas delas (ansible.posix e community.general).
ansible --versionO resultado correto começa com uma linha como ansible [core 2.19.x] e indica o Python utilizado; qualquer versão atual do core serve para este procedimento. ansible: command not found significa que o ~/.local/bin ainda não está no seu PATH — abra um novo shell ou use source ~/.bashrc.
Este é o processo completo de instalação. Os alvos não recebem nada.
Passo 2: Acesso via chave SSH para cada alvo
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20Em seguida, teste uma vez por host:
ssh root@10.0.0.10 true && echo okEssa linha executa duas funções: confirma que a autenticação por chave funciona sem senha e registra a host key em known_hosts. Faça isso agora, pois o Ansible exibe uma host key não registrada como um prompt interativo no meio da execução, o que parece um travamento do processo.
Passo 3: o inventário — INI primeiro, YAML quando crescer
O inventário é um arquivo de texto que lista as máquinas que o Ansible pode acessar. Crie inventory.ini em um novo diretório de projeto:
[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20
[vps:vars]
ansible_user=rootweb1 é um alias que você escolhe — é o que aparece no output e o que você utiliza com --limit web1. ansible_host é o endereço real. [vps] é um grupo, e [vps:vars] define variáveis para cada host no grupo; ansible_user é o usuário de login do Ansible. Ao lado, um ansible.cfg para você nunca mais precisar digitar -i:
[defaults]
inventory = inventory.iniO Ansible lê o ansible.cfg do diretório atual. O mesmo inventário em YAML — salve-o como inventory.yml e aponte o ansible.cfg para esse nome — é o que você preferirá quando cada host possuir várias variáveis:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootEles são equivalentes. O formato INI é mais fácil de visualizar com dois servidores; o YAML escala melhor com vinte. Escolha um e não se preocupe mais com isso.
Passo 4: comandos ad-hoc — o ping verde que prova tudo
ansible all -m pingIsso não é ICMP. O módulo ping é um ensaio completo: login SSH, cópia do módulo, execução de Python no destino e limpeza. O resultado correto é verde, com um bloco por host:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}O SUCCESS verde significa que a autenticação, o interpretador Python e o transporte funcionam — o playbook também funcionará. O UNREACHABLE! vermelho significa que o transporte falhou antes da execução de qualquer módulo; a string exata e a correção estão na seção de modos de falha abaixo. Mais dois comandos ad-hoc importantes:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomeAd-hoc serve para tarefas únicas e verificações. Qualquer comando que você executaria duas vezes deve estar em um playbook.
Passo 5: o primeiro playbook — o checklist de novo-VPS como código
Este é o conjunto de ações que você faria manualmente nos primeiros dez minutos em um novo servidor. Salve como site.yml:
---
- name: Baseline a fresh Ubuntu VPS
hosts: vps
become: true
vars:
deploy_user: deploy
deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
baseline_packages:
- fail2ban
- unattended-upgrades
- ufw
baseline_services:
- fail2ban
- unattended-upgrades
tasks:
- name: Create the deploy user
ansible.builtin.user:
name: "{{ deploy_user }}"
groups: sudo
append: true
shell: /bin/bash
- name: Install the deploy user's SSH key
ansible.posix.authorized_key:
user: "{{ deploy_user }}"
key: "{{ deploy_pubkey }}"
- name: Passwordless sudo for the deploy user
ansible.builtin.copy:
dest: /etc/sudoers.d/deploy
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: "0440"
validate: /usr/sbin/visudo -cf %s
- name: Install baseline packages
ansible.builtin.apt:
name: "{{ baseline_packages }}"
state: present
update_cache: true
- name: Enable and start baseline services
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop: "{{ baseline_services }}"
- name: Harden sshd with a drop-in
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/00-hardening.conf
content: |
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
X11Forwarding no
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart ssh
- name: Allow OpenSSH through ufw
community.general.ufw:
rule: allow
name: OpenSSH
- name: Enable ufw with default deny
community.general.ufw:
state: enabled
policy: deny
handlers:
- name: Restart ssh
ansible.builtin.service:
name: ssh
state: restartedLinhas importantes para entender em vez de apenas copiar:
Variables ficam sob vars: e são referenciadas com "{{ deploy_user }}" — use aspas em toda a expressão se o valor começar com uma chave, caso contrário o parser YAML lerá incorretamente. O lookup('file', ...) lê sua chave pública na máquina de control em tempo de execução, portanto o playbook não contém dados de chaves.
The loop. O loop: "{{ baseline_services }}" executa a tarefa do serviço uma vez por item, e a saída mostra cada item em sua própria linha. Note que a tarefa apt processa a lista completa de pacotes de uma vez — uma transação apt é mais rápida e é o padrão preferido para pacotes; loops são para módulos que atuam em um único item por vez.
The handler é o conceito que você deve internalizar. notify: Restart ssh não significa "reiniciar ssh agora". Ele coloca o handler em fila, que é executado uma única vez ao final do play, e apenas se a tarefa que o notificou reportar changed. Execute o playbook novamente amanhã: o arquivo drop-in já estará correto, a tarefa de cópia reportará ok, e o sshd não será reiniciado. A linha validate: é a trava de segurança — o sshd valida o arquivo antes de substituir o antigo, então um erro de digitação falha a tarefa em vez de quebrar o daemon.
PermitRootLogin prohibit-password, não no — deliberadamente. Este playbook faz login como root com uma chave. O prohibit-password desativa logins de root por senha enquanto mantém o seu acesso ativo. Assim que o usuário de deploy estiver validado (ssh deploy@10.0.0.10 sudo true — o endereço puro, já que web1 é apenas um alias que o Ansible conhece), altere para ansible_user=deploy no inventory e restrinja para no em uma execução posterior. Realize o hardening em uma ordem que não te deixe sem acesso.
O prefixo 00- é importante. Para a maioria das palavras-chave, o sshd respeita a primeira ocorrência que processa, e o sshd_config do Ubuntu inclui o sshd_config.d/*.conf em ordem lexical antes do seu próprio corpo. Imagens cloud do Ubuntu 24.04 já trazem um 60-cloudimg-settings.conf nesse diretório, e provedores que habilitam logins por senha via cloud-init adicionam um 50-cloud-init.conf com PasswordAuthentication yes; nomear o nosso como 00-hardening.conf faz com que ele venha primeiro na ordenação e prevaleça sobre ambos.
A ordem das tarefas é a segurança do firewall. O Allow OpenSSH executa antes do Enable ufw com uma política de deny — o Ansible executa as tarefas estritamente na ordem listada, portanto a brecha existe antes da barreira ser levantada. O fail2ban não precisa de configuração para ser útil aqui; os padrões do Ubuntu monitoram o sshd nativamente, e o que as jails realmente fazem — e o que ajustar — é abordado no guia do fail2ban no Ubuntu 24.04.
Passo 6: dry run com --check, depois execute de fato
ansible-playbook site.yml --checkO modo check conecta, calcula o que seria feito e não altera nada. Verifique a contagem changed= no PLAY RECAP ao final — este é o número de tarefas que modificariam cada host. Uma observação importante: o modo check possui um limite estrutural quando uma tarefa posterior depende de alterações de uma tarefa anterior. A imagem padrão do Ubuntu Server já vem com o ufw instalado, então este playbook executa o dry-run sem erros — mas em uma imagem mínima sem ele, as tarefas do ufw falham no modo check, pois o modo check não instala o pacote e o módulo não tem o que chamar. Isso é uma limitação de dry runs, não um erro no seu playbook. Quando o plano estiver correto:
ansible-playbook site.ymlCada tarefa imprime uma linha por host — amarelo changed, verde ok — e o resumo deve ser:
PLAY RECAP *********************************************************************
web1 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Dez ok representam a coleta de dados mais oito tarefas mais o handler. Seu changed pode divergir do meu em um ou dois: a imagem padrão do Ubuntu já possui ufw e unattended-upgrades, e o fail2ban inicia assim que o apt o instala, então uma tarefa pode reportar legitimamente ok em sua primeira execução — o estado que ela declara já estar aplicado. Os números que devem ser zero são unreachable e failed. Uma nota sobre become: true: é apenas uma formalidade ao conectar como root, mas assim que você altera ansible_user para deploy, o sudo é aplicado — e o arquivo sudoers NOPASSWD que este playbook instala é o que evita o -K na sua linha de comando. Sem ele, você receberá Missing sudo password, detalhado abaixo.
Passo 7: execute duas vezes — o que é idempotência
Execute o mesmo comando novamente de imediato:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0, e ok diminuiu um porque o handler não notificado nunca foi executado. Nada foi reinstalado, o sshd não foi reiniciado e o ufw não foi alterado. Isso faz do playbook tanto uma ferramenta de auditoria quanto de provisionamento: adicione web3 ao inventory no próximo mês e execute novamente — a nova máquina será configurada e as máquinas antigas serão verificadas. Um changed diferente de zero em uma máquina que você não alterou indica drift, o que significa que alguém editou manualmente o que deveria ter sido editado no playbook.
A partir daqui, o padrão se acumula. O próximo playbook relevante configura um WireGuard VPN no mesmo VPS e restringe a regra do ufw para que o SSH responda apenas pelo túnel; depois disso, um que instala Docker e Compose em cada app server. Quando o site.yml ocupar mais de três telas, divida-o em roles — mas não antes disso.
Modos de falha e as strings que você verá
UNREACHABLE with Permission denied.
web1 | UNREACHABLE! => {
"changed": false,
"msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
"unreachable": true
}O transporte SSH falhou antes da execução de qualquer módulo: ansible_user está incorreto, a chave nunca foi copiada para o host ou a chave errada está sendo oferecida. Reproduza com o comando ssh root@10.0.0.10 simples e depois ssh -v para ver quais chaves foram oferecidas. Se o SSH com senha funciona mas o Ansible não, você pulou o ssh-copy-id.
Missing sudo password.
web1 | FAILED! => {
"msg": "Missing sudo password"
}Você definiu become: true, conectou como um usuário comum e esse usuário requer senha para o sudo. Adicione -K (--ask-become-pass) à linha de comando ou configure uma entrada NOPASSWD no sudoers para o usuário — que é exatamente o motivo pelo qual o playbook instala uma para o deploy antes de você alternar para ele.
error: externally-managed-environment. Você executou o pip contra o Python do sistema no Ubuntu 24.04. Detalhado no passo 1: use pipx, não pip, e não o --break-system-packages.
mapping values are not allowed in this context.
ERROR! Syntax Error while loading YAML.
mapping values are not allowed in this contextQuase sempre é indentação: uma chave em um nível incorreto ou falta de espaço após os dois pontos. O número da linha reportado aponta para perto do erro, não exatamente nele — verifique a linha anterior também. O erro correlato found character '\t' that cannot start any token significa que um tab foi inserido; o YAML não permite tabs. Torne o ansible-playbook site.yml --syntax-check um hábito antes de cada execução e configure seu editor para indentação de dois espaços em arquivos YAML.
/usr/bin/python3: not found. Raro em imagens padrão do Ubuntu 24.04, comum em imagens minimal ou netboot: a execução do módulo falha porque o destino não possui Python. Instale o Python usando o módulo raw, o único módulo que não exige nada no destino: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, e então execute o playbook novamente.
FAQ
Eu preciso instalar o Ansible nos servidores que ele gerencia?
Não. O Ansible é agentless: a máquina de controle envia pequenos módulos Python via SSH, executa-os e os remove. O alvo precisa apenas de python3 e acesso SSH, ambos já presentes em imagens padrão do Ubuntu. A única instalação feita em todo este guia ocorre na sua máquina de controle.
Por que o Ansible exibe "Permission denied (publickey)"?
O bloco UNREACHABLE! com Permission denied (publickey) significa que a autenticação SSH falhou antes do Ansible executar qualquer comando. Verifique se o ansible_user no inventory corresponde à conta configurada, se você executou ssh-copy-id para aquele host e se o comando ssh user@host faz login sem senha. O que resolver o comando ssh comum resolverá o Ansible, pois ambos utilizam o mesmo transporte.
O que significa idempotência no Ansible?
Uma tarefa declara um estado desejado — "este pacote está presente", "esta linha está neste arquivo" — em vez de uma ação a ser executada. Se o estado já estiver correto, o Ansible não faz nada e reporta ok em vez de changed. É por isso que rodar um playbook duas vezes exibe changed=0 na segunda vez, e por que uma reexecução é uma auditoria segura em vez de uma reinstalação arriscada.
Devo usar pip ou pipx para instalar o Ansible no Ubuntu 24.04?
pipx. O Ubuntu 24.04 marca o Python do sistema como gerenciado externamente, portanto o pip install ansible falha com error: externally-managed-environment por design. O pipx install --include-deps ansible instala o Ansible em um virtualenv isolado e expõe ansible, ansible-playbook e o restante no seu PATH de forma limpa.
Qual é a diferença entre os pacotes ansible e ansible-core?
O ansible-core é o motor contendo apenas os módulos ansible.builtin. O pacote ansible agrupa o core com coleções da comunidade curadas — incluindo ansible.posix (o módulo authorized_key) e community.general (o módulo ufw), ambos usados neste guia. Comece com o pacote completo; mude para o core mais coleções selecionadas apenas se houver necessidade.