No tutorial Instalação do Docker Engine no Debian, Oracle Linux e Ubuntu, com exceção da infraestrutura KVM/Libvirt (criada com Terraform), toda a implementação foi manual: roteamento no gateway, formatação dos discos extras, repositórios, pacotes e daemon.json. Este tutorial automatiza tudo isso com Ansible — quatro roles executadas por um único playbook, com tratamento por família de distribuição.
Pré-requisito: as mesmas quatro VMs do tutorial anterior (gateway + 3 hosts Docker), provisionadas pela parte 5 da série Terraform+KVM. O Ansible roda na estação de trabalho (Ubuntu Desktop 24.04), de onde os playbooks são disparados.
Cada seção do tutorial manual virou uma role:
| Passo manual | Role Ansible |
|---|---|
| nftables + ip_forward + NAT no gateway | gateway |
| parted/ext4 (Debian/Ubuntu) e LVM/XFS (Oracle Linux) | disk_prep |
| Repositório oficial + pacotes docker-ce | docker_install |
Grupo docker + /etc/docker/daemon.json dual-stack |
docker_config |
sudo apt update && sudo apt upgrade -y
sudo apt install software-properties-common -y
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible sshpass -y
ansible --version # ansible [core 2.21.3]
O PPA é necessário: o
ansibledos repositórios do Ubuntu 24.04 é bem mais antigo que a série 2.21. Note também que o módulodeb822_repository(usado adiante) exige Ansible recente e o pacotepython3-debiannos alvos — lição que aprendemos na prática (seção 7).
~/ansible/docker/
├── ansible.cfg
├── inventory/
│ └── hosts.yml
├── site.yml
└── roles/
├── gateway/tasks/main.yml
├── disk_prep/tasks/main.yml
├── docker_install/tasks/main.yml
└── docker_config/tasks/main.yml
mkdir -p ~/ansible/docker/inventory
cd ~/ansible/docker
mkdir -p roles/{gateway,disk_prep,docker_install,docker_config}/tasks
# ansible.cfg
[defaults]
inventory = inventory/hosts.yml
host_key_checking = False
Dois grupos — o gateway separado dos hosts Docker — com a variável docker_distro mapeando cada VM para o caminho do repositório da Docker Inc. (debian, rhel... veja a seção 5.3):
# inventory/hosts.yml
all:
children:
gateway_servers:
hosts:
gateway:
ansible_host: 10.16.0.230
docker_hosts:
hosts:
docker-deb:
ansible_host: 10.16.1.10
docker_distro: debian
docker-ol:
ansible_host: 10.16.1.11
docker_distro: oracle
docker-ub:
ansible_host: 10.16.1.12
docker_distro: ubuntu
vars:
ansible_user: suporte
ansible_ssh_private_key_file: ~/.ssh/kvm
ansible_python_interpreter: /usr/bin/python3
E o ProxyJump? Nenhuma configuração extra: o Ansible usa o OpenSSH por baixo, então o
Host 10.16.1.*+ProxyJump gatewaydo seu~/.ssh/configcontinua valendo. Ohost_key_checking = Falseevita a confirmação interativa de fingerprint no primeiro acesso — aceitável em laboratório descartável, mas evite em produção.
site.yml — dois plays---
- name: Configurar Gateway (NAT + Forwarding)
hosts: gateway
become: yes
roles:
- gateway
- name: Preparar discos e instalar Docker
hosts: docker_hosts
become: yes
roles:
- disk_prep
- docker_install
- docker_config
gateway — NAT sem digitar nft---
- name: Habilitar e iniciar nftables
systemd:
name: nftables
enabled: yes
state: started
- name: Habilitar IP Forwarding (IPv4 e IPv6)
sysctl:
name: "{{ item }}"
value: "1"
state: present
reload: yes
loop:
- net.ipv4.ip_forward
- net.ipv6.conf.all.forwarding
- name: Criar tabelas e chains NAT
command: "{{ item }}"
loop:
- nft add table ip nat
- 'nft add chain ip nat postrouting { type nat hook postrouting priority srcnat \; }'
- nft add table ip6 nat
- 'nft add chain ip6 nat postrouting { type nat hook postrouting priority srcnat \; }'
ignore_errors: yes # evita erro se já existir
- name: Adicionar regras de NAT
command: "{{ item }}"
loop:
- nft add rule ip nat postrouting oifname "enp1s0" masquerade
- nft add rule ip6 nat postrouting oifname "enp1s0" masquerade
ignore_errors: yes
- name: Persistir regras nftables
shell: nft list ruleset > /etc/nftables.conf
changed_when: true
- name: Recarregar nftables
systemd:
name: nftables
state: restarted
O módulo sysctl substitui o par echo | tee + sysctl -p do tutorial manual — e persiste em /etc/sysctl.d/ sozinho. Já os comandos nft add com ignore_errors: yes são a parte menos idempotente da automação (ver seção 7).
disk_prep — duas receitas, uma roleA role se bifurca pela família do SO (ansible_facts['os_family']): bloco ext4 para Debian/Ubuntu, bloco LVM+XFS para Oracle Linux — exatamente as duas receitas do tutorial manual.
---
- name: Instalar parted (Debian/Ubuntu)
ansible.builtin.apt:
name: parted
state: present
update_cache: true
when: ansible_facts['os_family'] == "Debian"
- name: Criar tabela GPT e partição (Debian/Ubuntu)
when: ansible_facts['os_family'] == "Debian"
block:
- name: Criar label GPT
ansible.builtin.command: parted -s /dev/vdb mklabel gpt
- name: Criar partição primária ext4
ansible.builtin.command: parted -s /dev/vdb mkpart primary ext4 0% 100%
- name: Formatar partição
ansible.builtin.filesystem:
fstype: ext4
dev: /dev/vdb1
- name: Criar diretório de montagem
ansible.builtin.file:
path: /var/lib/docker
state: directory
- name: Obter UUID do disco
ansible.builtin.command: blkid -o export /dev/vdb1
register: blkid_output
changed_when: false
- name: Extrair UUID
ansible.builtin.set_fact:
docker_disk_uuid: "{{ blkid_output.stdout | regex_search('UUID=.*') }}"
- name: Adicionar ao /etc/fstab
ansible.builtin.lineinfile:
path: /etc/fstab
line: "{{ docker_disk_uuid }} /var/lib/docker ext4 defaults 0 2"
backup: true
- name: Recarregar systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Montar /var/lib/docker
ansible.posix.mount:
path: /var/lib/docker
src: /dev/vdb1
fstype: ext4
state: mounted
- name: Preparar disco com LVM (Oracle Linux)
when: ansible_facts['os_family'] == "RedHat"
block:
- name: Criar PV
ansible.builtin.command: pvcreate /dev/vdb
args:
creates: /dev/mapper/VGdocker-LVdocker
- name: Criar VG
ansible.builtin.command: vgcreate VGdocker /dev/vdb
ignore_errors: true
- name: Criar LV
ansible.builtin.command: lvcreate -l 100%FREE -n LVdocker VGdocker
ignore_errors: true
- name: Formatar com XFS
ansible.builtin.filesystem:
fstype: xfs
dev: /dev/mapper/VGdocker-LVdocker
- name: Criar diretório de montagem
ansible.builtin.file:
path: /var/lib/docker
state: directory
- name: Obter UUID
ansible.builtin.command: blkid -o export /dev/mapper/VGdocker-LVdocker
register: blkid_output
changed_when: false
- name: Extrair UUID
ansible.builtin.set_fact:
docker_disk_uuid: "{{ blkid_output.stdout | regex_search('UUID=.*') }}"
- name: Adicionar ao /etc/fstab
ansible.builtin.lineinfile:
path: /etc/fstab
line: "{{ docker_disk_uuid }} /var/lib/docker xfs defaults 0 2"
backup: true
- name: Recarregar systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Montar /var/lib/docker
ansible.posix.mount:
path: /var/lib/docker
src: /dev/mapper/VGdocker-LVdocker
fstype: xfs
state: mounted
Pontos de atenção:
filesystem e ansible.posix.mount são idempotentes de verdade (não reformatam nem remontam o que já está correto); os parted/pvcreate/vgcreate/lvcreate via command dependem de creates:/ignore_errors:;lineinfile com backup: true substitui o cp /etc/fstab{,.dist} manual — a linha só é inserida se ainda não existir;register + regex_search, automatizando o que fazíamos com grep no shell.docker_install — o salto para o deb822Aqui a automação melhora o tutorial manual: em vez do echo "deb [...] ..." | tee (formato one-line legacy), usamos o módulo deb822_repository, que escreve o repositório no formato deb822 moderno (/etc/apt/sources.list.d/docker.sources) e baixa a chave GPG automaticamente via signed_by. A variável docker_distro do inventário resolve a URL certa para cada VM (linux/debian, linux/ubuntu).
---
# ============================================
# Debian / Ubuntu
# ============================================
- name: Instalar Docker (Debian/Ubuntu)
when: ansible_facts['os_family'] == "Debian"
block:
- name: Instalar dependências
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-debian
state: present
update_cache: true
- name: Adicionar repositório Docker (formato deb822)
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: "https://download.docker.com/linux/{{ docker_distro }}"
suites: "{{ ansible_facts['distribution_release'] }}"
components: stable
signed_by: "https://download.docker.com/linux/{{ docker_distro }}/gpg"
state: present
enabled: true
- name: Instalar pacotes Docker
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: true
# ============================================
# Oracle Linux / RHEL
# ============================================
- name: Instalar Docker (Oracle Linux)
when: ansible_facts['os_family'] == "RedHat"
block:
- name: Instalar dnf-utils
ansible.builtin.dnf:
name: dnf-utils
state: present
- name: Adicionar repositório Docker
ansible.builtin.command:
cmd: dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
creates: /etc/yum.repos.d/docker-ce.repo
- name: Instalar pacotes Docker
ansible.builtin.dnf:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
- name: Iniciar e habilitar Docker
ansible.builtin.systemd:
name: docker
state: started
enabled: true
# ============================================
# Comum a todos
# ============================================
- name: Adicionar usuário ao grupo docker
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
No Oracle Linux, creates: /etc/yum.repos.d/docker-ce.repo torna o dnf config-manager --add-repo idempotente. A tarefa final (grupo docker) roda para todas as famílias — equivalente ao usermod -aG docker $USER manual.
docker_config — o daemon.json dual-stack---
- name: Criar /etc/docker/daemon.json
copy:
dest: /etc/docker/daemon.json
content: |
{
"ipv6": true,
"ip6tables": true,
"fixed-cidr": "10.10.0.0/24",
"fixed-cidr-v6": "fd00:dead:beef::/64",
"default-address-pools": [
{"base": "10.20.0.0/16", "size": 24},
{"base": "fd00:cafe::/56", "size": 64}
]
}
owner: root
group: root
mode: '0644'
- name: Reiniciar Docker
systemd:
name: docker
state: restarted
O mesmo daemon.json do tutorial manual — sem alteração. (Refinamento futuro: usar notify/handler para só reiniciar o Docker quando o arquivo mudar de fato.)
# 1. Conectividade — todos devem responder "pong"
ansible all -m ping
gateway | SUCCESS => { "changed": false, "ping": "pong" }
docker-deb | SUCCESS => { "changed": false, "ping": "pong" }
docker-ol | SUCCESS => { "changed": false, "ping": "pong" }
docker-ub | SUCCESS => { "changed": false, "ping": "pong" }
# 2. Primeiro o gateway — as VMs internas precisam de internet
ansible-playbook site.yml --limit gateway
# 3. Validar NAT e conectividade a partir de uma VM interna
ansible gateway -m shell -a "sudo nft list ruleset"
ansible docker-deb -m shell -a "ping -c2 8.8.8.8"
# 4. Discos + Docker nas três VMs de uma vez
ansible-playbook site.yml --limit docker_hosts
Verificações finais, nas três VMs com um único comando cada:
$ ansible docker_hosts -m shell -a "df -hT | grep docker"
docker-deb | CHANGED | rc=0 >>
/dev/vdb1 ext4 32G 2,3M 30G 1% /var/lib/docker
docker-ub | CHANGED | rc=0 >>
/dev/vdb1 ext4 32G 244K 30G 1% /var/lib/docker
docker-ol | CHANGED | rc=0 >>
/dev/mapper/VGdocker-LVdocker xfs 32G 261M 32G 1% /var/lib/docker
$ ansible docker_hosts -m shell -a "docker version | grep version"
docker-deb | CHANGED | rc=0 >> API version: 1.55 / Go version: go1.26.5
docker-ol | CHANGED | rc=0 >> API version: 1.55 / Go version: go1.26.5
docker-ub | CHANGED | rc=0 >> API version: 1.55 / Go version: go1.26.5
Três distribuições, mesma versão do Docker, mesmo storage dedicado — sem abrir um único SSH interativo.
A primeira execução capturada em laboratório falhou e alertou — o código deste tutorial já incorpora as correções:
deb822_repositoryexigepython3-debianno alvo. O erroModule failed: python3-debian is not installed, and install_python_debian is Falsederrubou o play na primeira rodada. Correção:python3-debianentrou na lista de dependências (seção 5.4). Documentado na descrição do módulo — fácil de perder.
Facts de topo estão deprecados. O warning
INJECT_FACTS_AS_VARS ... will be removed from ansible-core 2.24apareceu porque a primeira versão usavawhen: ansible_os_family == "Debian". A forma futura — já usada aqui — éansible_facts['os_family'](dicionário, sem o prefixoansible_). Adiante-se à 2.24: escreva playbooks novos sempre assim.
E duas melhorias pendentes, caso o laboratório evolua:
nft add ... ignore_errors: yes funciona, mas mascara erros legítimos e duplicaria regras sem o ignore_errors. A forma robusta é o módulo community.general.nftables ou um template completo do /etc/nftables.conf;notify no copy + handler reiniciaria só quando o daemon.json mudasse.python3-debian