SSD Nodes Learn
Guias Matt ConnorPor Matt Connor · Atualizado 2026-07-24

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 o ssh root@host solicitar senha, o Ansible falhará.
  • No Ubuntu 24.04, o pip install ansible falha com error: 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 ansible
error: 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 ansible

Abra 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 --version

O 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.20

Em seguida, teste uma vez por host:

ssh root@10.0.0.10 true && echo ok

Essa 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=root

web1 é 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.ini

O 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: root

Eles 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 ping

Isso 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" --become

Ad-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: restarted

Linhas 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 --check

O 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.yml

Cada 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=0

Dez 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=0

changed=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 context

Quase 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.