Como usar o Packer para criar imagens de máquina imutáveis

1. Introdução ao conceito de imagens imutáveis e o papel do Packer

Imagens de máquina imutáveis representam um paradigma fundamental na infraestrutura moderna. Diferentemente do modelo mutável, onde servidores são atualizados e modificados ao longo do tempo (gerando o temido "configuration drift"), a infraestrutura imutável preconiza que uma vez que uma imagem é criada, ela nunca é alterada. Para aplicar uma atualização, uma nova imagem é construída e as instâncias antigas são substituídas.

O HashiCorp Packer é a ferramenta padrão-ouro para automatizar a criação dessas imagens. Ele funciona através de três componentes principais:
- Builders: responsáveis por criar a máquina base em diferentes plataformas (AWS, Azure, GCP, VMware, VirtualBox)
- Provisioners: executam scripts e ferramentas de configuração dentro da imagem durante sua construção
- Post-processors: manipulam o artefato final (compressão, exportação, push para registries)

Os cenários de uso são vastos: desde AMIs para EC2 na AWS, imagens Docker, snapshots de disco no GCP, até templates para ambientes on-premises com VMware.

2. Instalação e configuração inicial do Packer

A instalação do Packer é simples. No Linux/macOS:

# Linux (Ubuntu/Debian)
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer

# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/packer

# Verificar instalação
packer version

A estrutura básica de um template HCL2 inclui variáveis, sources e builds:

# variables.pkr.hcl
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "instance_type" {
  type    = string
  default = "t2.micro"
}

# sources.pkr.hcl
source "amazon-ebs" "ubuntu" {
  region        = var.aws_region
  instance_type = var.instance_type
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
}

# build.pkr.hcl
build {
  sources = ["source.amazon-ebs.ubuntu"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx"
    ]
  }
}

Para configurar credenciais na AWS, utilize variáveis de ambiente ou o arquivo ~/.aws/credentials:

export AWS_ACCESS_KEY_ID="seu-access-key"
export AWS_SECRET_ACCESS_KEY="seu-secret-key"
export AWS_DEFAULT_REGION="us-east-1"

3. Criando uma imagem base com um builder de nuvem (exemplo AWS)

Vamos criar um template completo para gerar uma AMI com Nginx e hardening básico:

# ubuntu-nginx.pkr.hcl
packer {
  required_plugins {
    amazon = {
      version = ">= 1.0.0"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

variable "ami_name" {
  type    = string
  default = "ubuntu-nginx-20.04-{{timestamp}}"
}

source "amazon-ebs" "ubuntu" {
  ami_name      = var.ami_name
  instance_type = "t2.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
  tags = {
    Name        = "NginxImage"
    Environment = "production"
    Version     = "1.0.0"
  }
}

build {
  sources = ["source.amazon-ebs.ubuntu"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update -y",
      "sudo apt-get upgrade -y",
      "sudo apt-get install -y nginx ufw",
      "sudo ufw allow 'Nginx HTTP'",
      "sudo ufw --force enable",
      "sudo systemctl enable nginx",
      "sudo rm -rf /var/www/html/index.nginx-debian.html",
      "echo '<h1>Imagem Imutável Packer</h1>' | sudo tee /var/www/html/index.html"
    ]
  }

  provisioner "shell" {
    script = "scripts/hardening.sh"
  }
}

O script de hardening pode incluir:

#!/bin/bash
# scripts/hardening.sh

# Desabilitar login root SSH
sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

# Remover pacotes desnecessários
sudo apt-get autoremove -y
sudo apt-get autoclean -y

# Configurar limites de kernel
echo "net.ipv4.tcp_syncookies = 1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.ip_forward = 0" | sudo tee -a /etc/sysctl.conf

4. Trabalhando com provisioners avançados

O provisioner file permite copiar artefatos locais para dentro da imagem:

provisioner "file" {
  source      = "./app/"
  destination = "/home/ubuntu/app/"
}

provisioner "file" {
  content     = templatefile("templates/nginx.conf.tpl", { server_name = "example.com" })
  destination = "/tmp/nginx.conf"
}

Para automação complexa, utilize o provisioner ansible:

provisioner "ansible" {
  playbook_file = "./playbooks/webserver.yml"
  ansible_env_vars = [
    "ANSIBLE_HOST_KEY_CHECKING=False"
  ]
  extra_arguments = [
    "--extra-vars", "nginx_port=8080"
  ]
}

Em ambientes Windows, o provisioner powershell é essencial:

provisioner "powershell" {
  inline = [
    "Install-WindowsFeature -Name Web-Server -IncludeManagementTools",
    "New-Item -Path 'C:\\inetpub\\wwwroot\\index.html' -ItemType File -Value '<h1>Windows Image Imutável</h1>'"
  ]
}

5. Customização com variáveis, templates e funções

Para gerenciar múltiplos ambientes, crie arquivos .pkrvars.hcl:

# dev.pkrvars.hcl
aws_region     = "us-east-1"
instance_type  = "t2.micro"
ami_prefix     = "dev-nginx"
environment    = "development"

# prod.pkrvars.hcl
aws_region     = "us-west-2"
instance_type  = "t3.medium"
ami_prefix     = "prod-nginx"
environment    = "production"

Utilize funções integradas para personalização:

variable "ami_name" {
  type    = string
  default = "nginx-${var.environment}-${regex_replace(timestamp(), "[-: ]", "")}"
}

source "amazon-ebs" "ubuntu" {
  ami_name = var.ami_name
  tags = {
    BuildID    = uuidv4()
    CreatedAt  = formatdate("YYYY-MM-DD hh:mm:ss", timestamp())
  }
}

Iterações com for permitem builds multiplataforma:

locals {
  regions = ["us-east-1", "us-west-2", "eu-west-1"]
}

source "amazon-ebs" "multi-region" {
  for_each = toset(local.regions)
  region   = each.key
  ami_name = "nginx-${each.key}-${timestamp()}"
  # ... outras configurações
}

build {
  for_each = source.amazon-ebs.multi-region
  sources  = [source.amazon-ebs.multi-region[each.key]]
}

6. Post-processors: otimização e distribuição das imagens

O post-processor manifest gera metadados essenciais:

build {
  sources = ["source.amazon-ebs.ubuntu"]

  post-processor "manifest" {
    output     = "manifest.json"
    strip_path = true
    custom_data = {
      build_user = "ci-pipeline"
      version    = "1.0.0"
    }
  }
}

Para exportar e comprimir artefatos:

post-processor "compress" {
  output = "ubuntu-nginx-{{timestamp}}.tar.gz"
}

post-processor "artifice" {
  files = ["ubuntu-nginx-*.tar.gz"]
}

Em pipelines de containers:

source "docker" "ubuntu" {
  image  = "ubuntu:20.04"
  commit = true
}

build {
  sources = ["source.docker.ubuntu"]

  provisioner "shell" {
    inline = ["apt-get update && apt-get install -y nginx"]
  }

  post-processor "docker-tag" {
    repository = "myregistry/nginx"
    tags       = ["latest", "1.0.0"]
  }

  post-processor "docker-push" {
    login          = true
    login_server   = "myregistry.azurecr.io"
    login_username = var.docker_username
    login_password = var.docker_password
  }
}

7. Integração do Packer com pipelines CI/CD

Exemplo de workflow no GitHub Actions:

# .github/workflows/packer-build.yml
name: Build AMI with Packer

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  validate-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Packer
        uses: hashicorp/setup-packer@main
        with:
          version: "latest"

      - name: Validate template
        run: |
          packer fmt -check .
          packer validate -var-file=prod.pkrvars.hcl .

      - name: Build AMI
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          packer build -var-file=prod.pkrvars.hcl \
            -var="ami_version=${{ github.sha }}" \
            ubuntu-nginx.pkr.hcl

Para GitLab CI:

# .gitlab-ci.yml
stages:
  - validate
  - build

packer-validate:
  stage: validate
  script:
    - packer fmt -check .
    - packer validate -var-file=prod.pkrvars.hcl .

packer-build:
  stage: build
  script:
    - packer build -var-file=prod.pkrvars.hcl \
        -var="ami_version=$CI_COMMIT_SHORT_SHA" \
        ubuntu-nginx.pkr.hcl
  only:
    - main

8. Boas práticas, segurança e manutenção de imagens imutáveis

Princípios fundamentais para imagens seguras e eficientes:

# Minimizar superfície de ataque
provisioner "shell" {
  inline = [
    "sudo apt-get remove --purge -y vim-tiny nano curl wget",
    "sudo apt-get autoremove --purge -y",
    "sudo rm -rf /var/log/*.log /var/cache/apt/archives/*.deb",
    "sudo passwd -l root"
  ]
}

# Gerenciamento de segredos com Vault
provisioner "shell" {
  environment_vars = [
    "VAULT_ADDR=${var.vault_addr}",
    "VAULT_TOKEN=${var.vault_token}"
  ]
  script = "scripts/fetch-secrets.sh"
}

Estratégias de rolling update em clusters Kubernetes:

# Atualizar deployment com nova imagem
kubectl set image deployment/nginx-deployment \
  nginx=$ECR_REPOSITORY:$AMI_VERSION

# Rollback se necessário
kubectl rollout undo deployment/nginx-deployment

Para ambientes Nomad:

job "nginx" {
  group "web" {
    task "nginx" {
      driver = "docker"
      config {
        image = "myregistry/nginx:${var.ami_version}"
      }
    }
  }
}

Lembre-se sempre de versionar suas imagens com tags semânticas e manter um registro claro do que mudou entre versões. O Packer, combinado com pipelines CI/CD, permite que equipes entreguem infraestrutura imutável de forma confiável e auditável.


Referências