SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-07

Tutorial Ansible: primeiro playbook em um VPS

Instale o Ansible com pipx no Ubuntu 24.04, crie inventário e playbook para proteger um VPS, e corrija os erros Permission denied e sudo.

O que você vai criar

Uma máquina de controlo com Ansible instalado e um ou mais VPS Ubuntu 24.04 novos, contendo apenas a imagem padrão. No final, terá um ficheiro de inventário que identifica os seus servidores, um ping ad hoc que comprova que a autenticação funciona de ponta a ponta e um playbook que executa como código toda a lista de verificação de um VPS novo: um utilizador de implementação com a sua chave SSH, sshd reforçado, fail2ban, atualizações automáticas e uma firewall que permite OpenSSH antes de bloquear tudo o resto. Aponte-o para um servidor ou para vinte. Execute-o duas vezes. Na segunda execução, nada será alterado. Esse é o objetivo.

Depois de quinze anos a aprovisionar VPS, posso descrever o padrão real: toda a gente configura manualmente os primeiros cinco servidores e depois perde um fim de semana no sexto, porque ninguém se lembra do que fez nos cinco primeiros. Este guia aprofunda o tema da gestão de vários servidores Linux. Consulte-o quando se aperceber de que está a escrever o mesmo apt install em três terminais.

O que o Ansible realmente é, em um parágrafo

O Ansible não usa agentes. Não há nenhum daemon para instalar nos servidores que ele gere: a máquina de controlo liga-se através de SSH normal, copia um pequeno módulo Python para o destino, executa-o, lê o JSON que ele imprime e elimina-o. A única coisa de que um destino precisa é python3, que todas as imagens Ubuntu padrão já incluem. A palavra importante é idempotente e significa algo simples: uma tarefa descreve um estado, não uma ação. state: present para um pacote significa “garantir que está instalado”, não “executar o instalador”. Se o estado já estiver garantido, o Ansible não altera nada e comunica o resultado como ok em vez de changed. Essa propriedade é a essência do produto. É ela que torna seguro voltar a executar um playbook, e as execuções repetidas seguras são o que transforma um script de shell em infraestrutura.

Pré-requisitos e problemas importantes desde o início

  • Uma máquina de controlo: o seu portátil ou uma VPS pequena. Parto do princípio de que usa Ubuntu 24.04; no macOS, o procedimento é idêntico depois de instalar o pipx a partir do Homebrew.
  • Uma ou mais VPS de destino com Ubuntu 24.04 em KVM, acessíveis como root. Nada é instalado nelas.
  • Autenticação por chave SSH em todos os destinos. O Ansible usa exatamente o mesmo método de autenticação que o seu comando ssh. Se ssh root@host pedir uma palavra-passe, o Ansible falha.
  • No Ubuntu 24.04, pip install ansible falha com error: externally-managed-environment. Isto é uma política deliberada da distribuição, não uma avaria. Use pipx.
  • Os espaços em YAML fazem parte da sintaxe. Uma indentação incorreta produz mapping values are not allowed in this context, e qualquer caráter de tabulação causa uma falha.
  • Mantenha uma sessão SSH funcional aberta em cada destino enquanto o playbook reforça a configuração do sshd. Todos os bloqueios que ajudei a resolver para clientes envolveram fechar a última sessão "para testar a partir de uma sessão limpa".

Passo 1: instale o Ansible na máquina de controlo com pipx, não com pip

O impulso habitual é pip3 install ansible. Numa imagem 24.04 verdadeiramente nova, esse comando falha um passo antes, com Command 'pip3' not found, but can be installed with: sudo apt install python3-pip, e instalar o pip apenas leva ao problema seguinte:

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 gerido externamente (PEP 668), por isso o pip não pode competir com o apt pelos mesmos ficheiros. Não use --break-system-packages; o nome da flag é explícito. A solução correta é o pipx, que fornece ao Ansible o seu próprio virtualenv isolado e coloca os binários no seu PATH:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Abra uma nova shell depois de pipx ensurepath para aplicar a alteração ao PATH. --include-deps não é uma opção decorativa: o pacote ansible não fornece scripts de consola próprios, ansible, ansible-playbook e os restantes são pontos de entrada da dependência ansible-core. Sem essa flag, o pipx recusa a instalação com No apps associated with package ansible or its dependencies. Instale também o pacote ansible, não apenas ansible-core. O pacote completo inclui as collections da comunidade, e este playbook usa módulos de duas delas (ansible.posix e community.general).

ansible --version

O resultado correto começa com uma linha semelhante a ansible [core 2.19.x] e indica o Python com que o Ansible é executado; qualquer versão atual do core é suficiente para tudo o que é descrito aqui. ansible: command not found significa que ~/.local/bin ainda não está no seu PATH: abra uma nova shell ou execute source ~/.bashrc.

Essa é toda a instalação. Os destinos não recebem nada.

Passo 2: Acesso por chave SSH a todos os destinos

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, confirme isso uma vez por host:

ssh root@10.0.0.10 true && echo ok

Essa linha cumpre duas funções: confirma que a autenticação por chave funciona sem palavra-passe e regista a chave do host em known_hosts. Faça isto agora, porque o Ansible apresenta uma chave de host não registada como um pedido interativo no meio de uma execução, o que parece exatamente um bloqueio.

Passo 3: o inventário, primeiro INI, YAML quando crescer

O inventário é um ficheiro de texto que lista as máquinas que o Ansible pode gerir. Crie inventory.ini num diretório de projeto novo:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 é um alias à sua escolha. É o nome apresentado na saída e o alvo usado com --limit web1. ansible_host é o endereço real. [vps] é um grupo, e [vps:vars] define variáveis para todos os hosts desse grupo; ansible_user é a conta com que o Ansible inicia sessão. Ao lado, adicione um ansible.cfg para nunca mais ter de escrever -i:

[defaults]
inventory = inventory.ini

O Ansible lê ansible.cfg do diretório atual. O mesmo inventário em YAML, guardado como inventory.yml e indicado em ansible.cfg em vez desse nome, será a sua opção preferida quando os hosts tiverem várias variáveis:

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

São equivalentes. O INI é mais fácil de consultar rapidamente com dois servidores; o YAML adapta-se melhor a vinte. Escolha um formato e não volte a preocupar-se com isso.

Etapa 4: comandos ad hoc, o pong verde que confirma que tudo funciona

ansible all -m ping

Isto não é ICMP. O módulo ping é um ensaio completo: início de sessão 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 resultado verde SUCCESS significa que a autenticação, o interpretador Python e o transporte funcionam, e que o playbook também funcionará. O resultado vermelho UNREACHABLE! significa que o transporte falhou antes da execução de qualquer módulo; a mensagem exata e a correção estão na secção sobre modos de falha abaixo. Há mais dois comandos ad hoc que vale a pena conhecer:

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

Use comandos ad hoc para ações pontuais e verificações. Tudo o que executaria duas vezes deve estar num playbook.

Passo 5: o primeiro playbook, a lista de verificação de um novo VPS como código

Isto é tudo o que faria manualmente nos primeiros dez minutos num servidor novo. Guarde-o 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

Vale a pena compreender estas linhas em vez de apenas as copiar:

As variáveis ficam em vars: e são referenciadas com "{{ deploy_user }}". Coloque toda a expressão entre aspas quando um valor começa com uma chaveta; caso contrário, o analisador YAML pode interpretá-la incorretamente. lookup('file', ...) lê a sua chave pública na máquina de control durante a execução, pelo que o playbook não contém material de chaves.

O loop. loop: "{{ baseline_services }}" executa a tarefa do serviço uma vez por item, e o resultado apresenta cada item na sua própria linha. Note que a tarefa apt recebe toda a lista de pacotes de uma só vez. Uma transação apt é mais rápida e é o padrão recomendado para pacotes; use loops em módulos que atuam efetivamente sobre um item de cada vez.

O handler é o conceito que deve compreender. notify: Restart ssh não significa "reiniciar o ssh agora". Coloca o handler numa fila. Este é executado uma vez no final do play e apenas se a tarefa que o notificou tiver indicado changed. Se executar novamente o playbook amanhã, o ficheiro drop-in já estará correto, a tarefa de cópia indicará ok e o sshd nunca será reiniciado. A linha validate: fornece segurança ao acionar o serviço. O sshd verifica o ficheiro antes de substituir o antigo, pelo que um erro de sintaxe faz a tarefa falhar em vez de interromper o daemon.

PermitRootLogin prohibit-password, não no, de propósito. Este playbook inicia sessão como root com uma chave. prohibit-password desativa as sessões root com palavra-passe e mantém a sua sessão por chave ativa. Depois de confirmar que o utilizador de implementação funciona (ssh deploy@10.0.0.10 sudo true, o endereço direto, porque web1 é apenas um alias que o Ansible conhece), altere ansible_user=deploy no inventário e restrinja-o a no numa execução posterior. Faça o endurecimento numa ordem que não o deixe sem acesso.

O prefixo 00- é importante. Para a maioria das palavras-chave, o sshd respeita a primeira ocorrência que analisa, e o sshd_config do Ubuntu inclui sshd_config.d/*.conf por ordem lexicográfica antes do próprio corpo. As imagens cloud do Ubuntu 24.04 já incluem um 60-cloudimg-settings.conf nesse diretório, e os provedores que ativam sessões com palavra-passe através do cloud-init adicionam um 50-cloud-init.conf com PasswordAuthentication yes. Dar ao nosso ficheiro o nome 00-hardening.conf faz com que seja ordenado primeiro e prevaleça sobre ambos.

A ordem das tarefas é a proteção do firewall. Allow OpenSSH é executado antes de Enable ufw com uma política de negação. O Ansible executa as tarefas rigorosamente pela ordem apresentada, pelo que a abertura existe antes de a barreira ser aplicada. O fail2ban não precisa de configuração para ser útil neste caso; as predefinições do Ubuntu monitorizam o sshd imediatamente. O que as jails fazem efetivamente e o que deve ajustar é explicado no guia do fail2ban no Ubuntu 24.04.

Passo 6: faça uma execução de teste com --check e depois execute de verdade

ansible-playbook site.yml --check

O modo de verificação estabelece a ligação, calcula o que faria e não altera nada. Leia a contagem changed= no PLAY RECAP no final. Esse é o número de tarefas que modificariam cada host. Existe uma limitação estrutural importante: o modo de verificação não consegue lidar com situações em que uma tarefa posterior depende das alterações feitas por uma tarefa anterior. A imagem de servidor padrão do Ubuntu já inclui ufw, por isso este playbook é executado em modo de teste sem problemas. Numa imagem mínima sem ufw, as tarefas do ufw falham no modo de verificação, porque esse modo nunca instalou realmente o pacote e o módulo não tem nada para chamar. Essa é uma limitação das execuções de teste, não um erro do seu playbook. Quando o plano parecer correto:

ansible-playbook site.yml

Cada tarefa apresenta uma linha por host: changed a amarelo e ok a verde. 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

Esses ok correspondem à recolha de factos, às oito tarefas e ao handler. O seu changed pode diferir do meu em uma ou duas unidades. A imagem padrão do Ubuntu já inclui ufw e unattended-upgrades, e o fail2ban inicia automaticamente assim que o apt o instala. Por isso, uma tarefa pode legitimamente indicar ok na primeira execução, porque o estado que ela declara já estava aplicado. Os números que devem ser zero são unreachable e failed. Uma observação sobre become: true: isso é apenas uma formalidade enquanto se liga como root. No momento em que alterar ansible_user para deploy, o sudo passa a ser efetivamente utilizado, e o ficheiro sudoers com NOPASSWD instalado por este playbook é precisamente o que mantém -K fora da linha de comandos. Sem esse ficheiro, verá Missing sudo password, conforme explicado abaixo.

Passo 7: execute duas vezes e veja como funciona a idempotência

Execute imediatamente o mesmo comando novamente:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 e ok diminuíram em 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. É isto que torna o playbook tanto uma ferramenta de auditoria como de provisionamento: adicione web3 ao inventário no próximo mês e execute-o novamente; o novo servidor será configurado e os servidores antigos serão verificados. Um changed diferente de zero num servidor que não foi alterado indica divergência de configuração e mostra que alguém editou manualmente aquilo que deveria ter sido editado no playbook.

A partir daqui, o padrão continua. O próximo playbook que vale a pena escrever instala uma VPN WireGuard no mesmo VPS e restringe a regra do ufw para que o SSH responda apenas pelo túnel. Depois disso, escreva um playbook que instale Docker e Compose em todos os servidores de aplicações. Quando site.yml ultrapassar três ecrãs, divida-o em roles, mas não antes.

Modos de falha e as mensagens apresentadas

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 de qualquer módulo ser executado: ansible_user está incorreto, a chave nunca foi copiada para esse host ou está a ser oferecida a chave errada. Reproduza o problema com ssh root@10.0.0.10 simples e, em seguida, use ssh -v para ver quais chaves foram oferecidas. Se o SSH com palavra-passe funcionar mas o Ansible não, não definiu ssh-copy-id.

Missing sudo password.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Definiu become: true, ligou-se como um utilizador que não é root e esse utilizador precisa de uma palavra-passe para usar sudo. Adicione -K (--ask-become-pass) à linha de comandos ou atribua ao utilizador uma entrada NOPASSWD no sudoers. É precisamente por isso que o playbook instala uma entrada para deploy antes de mudar para esse utilizador.

error: externally-managed-environment. Executou pip no Python do sistema no Ubuntu 24.04. Isto é explicado no passo 1: use pipx, não pip, e nã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 é um problema de indentação: uma chave está no nível errado ou falta um espaço depois de dois-pontos. O número da linha indicado aponta para perto do erro, não necessariamente para o erro. Verifique também a linha anterior. A mensagem relacionada found character '\t' that cannot start any token significa que foi introduzido um tabulador. O YAML não permite tabuladores. Torne ansible-playbook site.yml --syntax-check um procedimento automático antes de cada execução e configure o editor para usar indentação de dois espaços em ficheiros YAML.

/usr/bin/python3: not found. É raro em imagens Ubuntu 24.04 padrão, mas comum em imagens mínimas ou netboot. A execução do módulo falha porque o destino não tem Python. Instale-o com o módulo raw, o único módulo que não precisa de nada no sistema remoto: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become. Em seguida, execute novamente o playbook.

FAQ

Preciso instalar o Ansible nos servidores que ele gere?

Não. O Ansible não requer agentes: a máquina de controlo envia pequenos módulos Python por SSH, executa-os e remove-os. O destino precisa apenas de python3 e de acesso SSH, ambos já disponíveis nas imagens padrão do Ubuntu. A única instalação em todo este guia é feita na máquina de controlo.

Por que motivo o Ansible apresenta "Permission denied (publickey)"?

O bloco UNREACHABLE! com Permission denied (publickey) indica que a autenticação SSH falhou antes de o Ansible executar qualquer operação. Confirme se ansible_user no inventário corresponde à conta que configurou, se executou ssh-copy-id para esse host e se o comando ssh user@host simples inicia sessão sem pedir uma palavra-passe. A correção do comando ssh simples corrige também o Ansible, porque ambos usam o mesmo transporte.

O que significa idempotente no Ansible?

Uma tarefa declara um estado pretendido, como "este pacote está presente" ou "esta linha está neste ficheiro", em vez de declarar uma ação a executar. Se o estado já existir, o Ansible não faz nada e apresenta ok em vez de changed. Por isso, executar um playbook duas vezes apresenta changed=0 na segunda execução. Uma nova execução funciona como uma auditoria segura, e não como 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 gerido externamente, por isso pip install ansible falha com error: externally-managed-environment por definição. pipx install --include-deps ansible instala o Ansible num virtualenv isolado e disponibiliza ansible, ansible-playbook e os restantes comandos no seu PATH de forma limpa.

Qual é a diferença entre os pacotes ansible e ansible-core?

ansible-core é o motor com apenas os módulos ansible.builtin. O pacote ansible inclui o core e as collections comunitárias selecionadas, incluindo ansible.posix (o módulo authorized_key) e community.general (o módulo ufw), ambos utilizados neste guia. Comece pelo pacote completo. Reduza para o core com collections escolhidas manualmente apenas quando tiver um motivo para o fazer.