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. Sessh root@hostpedir uma palavra-passe, o Ansible falha. - No Ubuntu 24.04,
pip install ansiblefalha comerror: 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 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 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 ansibleAbra 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 --versionO 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.20Em seguida, confirme isso uma vez por host:
ssh root@10.0.0.10 true && echo okEssa 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=rootweb1 é 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.iniO 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: rootSã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 pingIsto 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" --becomeUse 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: restartedVale 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 --checkO 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.ymlCada 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=0Esses 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=0changed=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 contextQuase 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.