O problema
Toda vez que uma máquina nova precisa subir e o processo depende de alguém lembrar os passos certos, você já tem um problema. Foi exatamente isso que eu vivi trabalhando com EC2 dedicadas: cada instância nova virava uma pequena novela. Subir a máquina, instalar pacote por pacote na mão, ajustar configuração, esquecer algum passo, documentar (às vezes) num lugar que ninguém revisita depois, e torcer para lembrar de fazer tudo igual da próxima vez.
Isso não é só perda de tempo, é falta de padrão. Duas máquinas que deveriam ser idênticas nunca são de fato idênticas, porque o processo de criação delas nunca foi de fato o mesmo. E quando algo dá errado, não tem como saber com certeza o que está instalado e configurado ali sem entrar na máquina e investigar manualmente.
A pergunta que me guiou foi simples: como transformar "instalar uma máquina" em algo repetível, versionado e auditável, sem depender de alguém lembrar os passos certos?
A solução: Packer + Ansible-local
A combinação que resolveu isso pra mim foi:
- Packer para orquestrar o build da imagem (AMI)
- Ansible rodando em modo local (
ansible-local) dentro da própria instância de build, aplicando os playbooks diretamente na máquina, sem precisar de um control node externo fazendo SSH
O fluxo básico fica assim:
- Packer sobe uma instância temporária a partir de uma AMI base
- Copia os playbooks Ansible para dentro da instância
- Executa o Ansible localmente, aplicando os roles definidos
- Roda os passos de limpeza (remove chaves temporárias, limpa logs, zera histórico de bash, remove credenciais de build)
- Congela tudo numa AMI nova, versionada
Um exemplo simplificado do template, em HCL2:
source "amazon-ebs" "app_base" {
ami_name = "app-golden-{{timestamp}}"
instance_type = "t3a.large"
region = "sa-east-1"
source_ami_filter {
filters = {
name = "al2023-ami-*-arm64"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["amazon"]
most_recent = true
}
ssh_username = "ec2-user"
}
build {
sources = ["source.amazon-ebs.app_base"]
provisioner "shell" {
inline = ["sudo mkdir -p /etc/ansible/facts.d"]
}
provisioner "ansible-local" {
playbook_file = "playbooks/site.yml"
role_paths = ["roles/hardening", "roles/app-setup", "roles/config-sync"]
}
provisioner "shell" {
script = "scripts/cleanup.sh"
}
}
O ponto central é que o Ansible não toca a máquina de fora, ele roda dentro dela, como parte do build. Isso elimina a dependência de conectividade SSH externa durante o processo e faz o build funcionar de forma consistente em qualquer conta ou VPC, já que não precisa abrir rota de rede para um control node.
Um detalhe que fez diferença: Packer se conectando via SSM
Um ponto que vale a pena registrar é que o builder amazon-ebs do Packer não depende só de SSH tradicional para se conectar na instância de build. Existe a opção de usar o Session Manager (SSM) como interface de conexão, configurando ssh_interface = "session_manager" no builder.
Na prática, o Packer sobe um túnel via SSM até a porta de SSH da instância (por padrão 22) e conversa com ela por dentro desse túnel, sem precisar que a instância tenha uma rota de rede pública nem uma Security Group liberando a porta 22 para o seu IP. Para isso funcionar é necessário:
- Anexar um Instance Profile válido com permissão de Systems Manager na instância de build (para o SSM Agent conseguir abrir a sessão)
- Ter o session-manager-plugin instalado na máquina local que está rodando o Packer
- Manter
communicator = "ssh", já que o SSM aqui só substitui a forma como o túnel é estabelecido, o comunicador continua sendo SSH por baixo dos panos
Um exemplo de builder usando esse modelo:
source "amazon-ebs" "app_base_ssm" {
ami_name = "app-golden-{{timestamp}}"
instance_type = "m6a.large"
region = "sa-east-1"
communicator = "ssh"
ssh_username = "ec2-user"
ssh_interface = "session_manager"
iam_instance_profile = "packer-ssm-instance-profile"
source_ami_filter {
filters = {
name = "al2023-ami-*-arm64"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["amazon"]
most_recent = true
}
}
Isso resolveu um incômodo real: builds de imagem deixaram de depender de sub-redes com saída para a internet ou de Security Groups abrindo porta 22 pontualmente durante o processo. A instância de build fica isolada, sem exposição de SSH, e o Packer se conecta por dentro da rede da AWS usando as mesmas permissões IAM que já governam o resto do ambiente. Menos superfície de ataque durante o processo de build, que é justamente o momento em que a máquina está mais "aberta" para receber comandos externos.
O pulo do gato: configuração fora da imagem
Aqui está a parte que, na minha visão, separa "automatizei o install" de "resolvi o problema de verdade": a imagem não carrega a configuração da aplicação embutida.
O Packer builda a AMI com tudo que é estrutural: pacotes, hardening, usuários, estrutura de diretórios, serviços systemd. Mas a configuração específica da aplicação (endpoints, parâmetros de conexão, feature flags, e por aí vai) fica no AWS Systems Manager Parameter Store, como fonte única de verdade.
Quando a instância sobe de fato em produção, fora do contexto de build, um serviço systemd dispara na inicialização, busca os parâmetros correspondentes ao ambiente/aplicação no Parameter Store e aplica a configuração local:
[Unit]
Description=Sync config from Parameter Store
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/app/bin/config-sync.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
#!/usr/bin/env bash
set -euo pipefail
APP_ENV=$(cat /etc/app/environment)
PREFIX="/app/${APP_ENV}/config"
aws ssm get-parameters-by-path \
--path "${PREFIX}" \
--recursive \
--with-decryption \
--query "Parameters[*].[Name,Value]" \
--output text | while read -r name value; do
key=$(basename "${name}")
echo "${key}=${value}" >> /etc/app/app.env
done
systemctl restart app.service
Isso separa claramente duas responsabilidades:
- Packer/Ansible cuidam do "o quê está instalado" (imutável, versionado, buildado uma vez)
- Parameter Store cuida do "como está configurado agora" (mutável, centralizado, com revisão de histórico nativa do SSM)
Na prática, isso trouxe ganhos que não eram o objetivo inicial, mas viraram os mais valiosos:
- Rastreabilidade de configuração: todo parâmetro tem histórico de mudança no próprio Parameter Store, então dá pra saber quem mudou o quê e quando, sem precisar caçar em log de aplicação
- Configuração centralizada por ambiente: a mesma AMI serve para DEV, UAT e PROD, o que muda é só o path que ela lê no Parameter Store
- Redeploy de configuração sem rebuild de imagem: ajustar um parâmetro não exige gerar uma AMI nova, só reiniciar o serviço de sync (ou deixar o próximo boot pegar)
O efeito colateral bom: Disaster Recovery
Esse foi o ponto que só ficou óbvio depois que a arquitetura já estava rodando: uma vez que você tem a golden AMI e a configuração centralizada e desacoplada no Parameter Store, replicar a aplicação para outra AZ ou outra região vira um problema de infraestrutura, não de aplicação.
O racional é direto:
- A AMI já tem tudo que a aplicação precisa para rodar (pacotes, hardening, serviços)
- A configuração não está embutida na imagem, está no Parameter Store, que também pode ser replicado ou lido cross-region, dependendo do desenho
- Logo, subir a aplicação numa nova AZ ou região é: copiar a AMI para o destino, subir a instância, deixar o
config-syncbuscar a configuração certa
Isso transformou o cenário de DR de "vamos ter que reconstruir isso na mão e torcer para lembrar de tudo" para "isso já é testado e replicável, porque é o mesmo processo usado todo dia para gerar imagem nova". Fizemos inclusive simulações de falha de AZ cobrindo essa camada, e a parte provisionada via Packer foi de longe a mais previsível de recuperar, justamente por não depender de passos manuais.
Hardening: CIS Level 1/2 embutido no build
Outro ganho direto de ter o Ansible rodando como parte do processo de build: hardening deixou de ser uma tarefa "depois que a máquina já está no ar" e virou parte do próprio pipeline de criação da imagem.
Os controles de CIS Benchmark, com Level 1 como baseline e alguns controles de Level 2 aplicados pontualmente onde fazia sentido, viraram uma role Ansible própria, aplicada em todo build:
- name: hardening
hosts: localhost
connection: local
become: true
roles:
- role: cis-benchmark
vars:
cis_level: 1
cis_al2023_specific: true
Isso significa que toda AMI nova já nasce hardenizada, sem depender de rodar um scanner depois e corrigir desvio por desvio. O hardening é parte do artefato, não um processo paralelo.
Migração Amazon Linux 2 → Amazon Linux 2023
A mesma estrutura de Packer + Ansible foi o que viabilizou a migração de AL2 para AL2023 sem drama. Como o pipeline já era declarativo, roles Ansible descrevendo o estado desejado da máquina, e não passos manuais amarrados a uma distro específica, trocar a AMI base de origem e ajustar os pontos de incompatibilidade (principalmente em torno de pacotes renomeados e mudança de cgroups) foi um trabalho localizado, não um retrabalho do zero.
Isso reforça um ponto que vale destacar: o valor de ter Packer + Ansible não é só economizar tempo hoje, é ter a distro e a versão do sistema operacional como parâmetro do pipeline, não como algo enraizado no processo manual de alguém.
| Antes | Depois |
|---|---|
| Instalação manual, sem processo padronizado | Build automatizado e versionado via Packer |
| Conexão de build dependendo de SSH exposto | Build via SSM, sem exposição de porta 22 |
| Configuração embutida/ad-hoc na máquina | Configuração centralizada no Parameter Store, aplicada no boot |
| DR dependente de reconstrução manual | DR via replicação de AMI + config-sync automático |
| Hardening aplicado depois, sob demanda | Hardening (CIS) embutido no build |
| Migração de distro = retrabalho grande | Migração de distro = ajuste pontual no pipeline |
O maior aprendizado prático desse case não foi técnico, foi de desenho: a decisão de não colocar a configuração de aplicação dentro da imagem foi o que abriu espaço para os ganhos de DR e centralização. Se a configuração estivesse embutida na AMI, cada mudança de parâmetro exigiria rebuild, e a imagem deixaria de ser "golden" para virar só mais um artefato acoplado a um ambiente específico.
Packer resolve o "como eu construo isso de forma repetível". Ansible-local resolve o "como eu aplico o estado desejado sem depender de infraestrutura externa durante o build". A conexão via SSM resolve o "como eu faço isso sem expor a instância de build". E o Parameter Store resolve o "como eu mantenho isso configurável sem quebrar a imutabilidade da imagem". Separados, cada um resolve uma fatia do problema. Juntos, o resultado foi bem maior que a soma das partes.
Top comments (0)