Integração com Pipelines CI/CD e Solução de Problemas com Packer
Nota: Este tutorial é o sexto e último de uma série de 6 tutoriais sobre criação de imagens QEMU/KVM com Packer. Se você ainda não leu os tutoriais anteriores, recomendamos que o faça antes de prosseguir. Criação de Imagem Ubuntu com Packer e Cloud-init.
Introdução
Nos tutoriais anteriores, exploramos os fundamentos do Packer e QEMU/KVM, aprendemos a criar imagens automatizadas para diferentes distribuições Linux (Oracle Linux, Debian e Ubuntu) e dominamos técnicas avançadas de personalização. Agora, chegamos ao último tutorial da série, onde vamos abordar dois tópicos cruciais para ambientes profissionais:
- Integração com Pipelines CI/CD: Como automatizar a criação de imagens como parte de um fluxo de integração e entrega contínua
- Solução de Problemas Avançados: Técnicas para diagnosticar e resolver problemas comuns e complexos com o Packer
A integração do Packer em pipelines CI/CD permite automatizar completamente o processo de criação de imagens, garantindo consistência, rastreabilidade e facilitando a adoção de práticas de infraestrutura como código (IaC). Já o domínio de técnicas avançadas de solução de problemas é essencial para lidar com situações inesperadas e garantir a robustez do seu processo de criação de imagens.
Neste tutorial, vamos:
- Entender os princípios de CI/CD aplicados à criação de imagens
- Implementar pipelines em diferentes plataformas (GitLab CI, GitHub Actions e Jenkins)
- Criar estratégias de teste para validar imagens
- Explorar técnicas avançadas de depuração e solução de problemas
- Aprender a otimizar pipelines para maior eficiência
Princípios de CI/CD para Criação de Imagens
Antes de mergulharmos nas implementações específicas, vamos entender os princípios fundamentais de CI/CD aplicados à criação de imagens:
1. Versionamento de Código
Todo o código relacionado à criação de imagens deve ser versionado em um sistema de controle de versão como Git:
- Arquivos de configuração do Packer (.pkr.hcl)
- Scripts de provisionamento
- Arquivos de automação (Kickstart, Preseed, Cloud-init)
- Arquivos de configuração de serviços
- Pipelines CI/CD
2. Automação Completa
O processo de criação de imagens deve ser completamente automatizado, sem intervenção manual:
- Download de ISOs
- Validação de configurações
- Build de imagens
- Testes
- Publicação de imagens
3. Testes Automatizados
As imagens geradas devem ser testadas automaticamente para garantir sua qualidade:
- Testes de inicialização
- Verificação de serviços
- Testes de segurança
- Validação de configurações
4. Imutabilidade
As imagens devem ser tratadas como artefatos imutáveis:
- Uma vez criada, uma imagem não deve ser modificada
- Alterações devem resultar em novas versões de imagens
- Versões anteriores devem ser preservadas
5. Rastreabilidade
Deve ser possível rastrear cada imagem até seu código-fonte:
- Metadados de versão
- Commit que gerou a imagem
- Parâmetros de build
- Resultados de testes
Preparando o Ambiente
Vamos preparar nosso ambiente de trabalho para este tutorial:
1
2
3
4
5
6
# Criar diretório para o sexto tutorial
mkdir -p ~/packer-kvm-tutorial/tutorial6
cd ~/packer-kvm-tutorial/tutorial6
# Criar subdiretórios necessários
mkdir -p http scripts tests ci
Estrutura de Projeto para CI/CD
Para facilitar a integração com pipelines CI/CD, vamos organizar nosso projeto com a seguinte estrutura:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.
├── README.md                  # Documentação do projeto
├── packer/                    # Arquivos do Packer
│   ├── ubuntu/                # Configuração para Ubuntu
│   │   ├── ubuntu.pkr.hcl     # Arquivo principal do Packer
│   │   ├── variables.pkr.hcl  # Variáveis do Packer
│   │   └── http/              # Arquivos HTTP (Cloud-init)
│   ├── debian/                # Configuração para Debian
│   └── oracle/                # Configuração para Oracle Linux
├── scripts/                   # Scripts de provisionamento
│   ├── common/                # Scripts comuns a todas as distribuições
│   ├── ubuntu/                # Scripts específicos para Ubuntu
│   ├── debian/                # Scripts específicos para Debian
│   └── oracle/                # Scripts específicos para Oracle Linux
├── tests/                     # Testes automatizados
│   ├── boot_test.sh           # Teste de inicialização
│   ├── services_test.sh       # Teste de serviços
│   └── security_test.sh       # Teste de segurança
└── ci/                        # Configurações de CI/CD
    ├── gitlab-ci.yml          # Configuração do GitLab CI
    ├── github-workflow.yml    # Configuração do GitHub Actions
    └── Jenkinsfile            # Configuração do Jenkins
Vamos criar alguns desses arquivos para demonstrar a integração com CI/CD.
Arquivo Packer Otimizado para CI/CD
Primeiro, vamos criar um arquivo Packer otimizado para CI/CD:
1
2
mkdir -p packer/ubuntu/http
nano packer/ubuntu/ubuntu.pkr.hcl
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
packer {
  required_version = ">= 1.10.0" 
  required_plugins {
    qemu = {
      version = "= 1.1.0"
      source  = "github.com/hashicorp/qemu"
    }
  }
}
# Variáveis locais para uso interno
locals {
  build_timestamp = formatdate("YYYYMMDDhhmmss", timestamp())
  image_name      = "${var.image_prefix}-${var.ubuntu_version}-${local.build_timestamp}"
}
# Fonte da imagem
source "qemu" "ubuntu" {
  vm_name          = "${local.image_name}.raw"
  iso_url          = var.iso_url
  iso_checksum     = var.iso_checksum
  disk_size        = var.disk_size
  memory           = var.memory
  cpus             = var.cpus
  disk_image       = false
  output_directory = var.output_directory
  accelerator      = "kvm"
  disk_interface   = "virtio"
  format           = "raw"
  net_device       = "virtio-net"
  boot_wait        = "5s"
  boot_command     = var.boot_command
  http_directory   = "${path.root}/http"
  cpu_model        = "host"
  shutdown_command = "echo '${var.ssh_password}' | sudo -S shutdown -P now"
  ssh_username     = var.ssh_username
  ssh_password     = var.ssh_password
  ssh_timeout      = var.ssh_timeout
  
  # Adicionar metadados à imagem
  qemu_binary      = "qemu-system-x86_64"
  qemuargs         = [
    ["-smbios", "type=1,serial=${var.image_prefix}-${var.ubuntu_version}-${local.build_timestamp}"]
  ]
}
# Build da imagem
build {
  name    = "ubuntu-server"
  sources = ["source.qemu.ubuntu"]
  # Adicionar metadados à imagem
  provisioner "shell" {
    inline = [
      "echo '${var.image_prefix}-${var.ubuntu_version}-${local.build_timestamp}' | sudo tee /etc/image_version",
      "echo 'Build Date: ${timestamp()}' | sudo tee -a /etc/image_version",
      "echo 'Builder: Packer ${packer.version}' | sudo tee -a /etc/image_version"
    ]
  }
  # Executar scripts de provisionamento
  provisioner "shell" {
    scripts = var.provisioning_scripts
  }
  # Converter para QCOW2 e comprimir
  post-processor "shell-local" {
    inline = [
      "qemu-img convert -O qcow2 -c ${var.output_directory}/${local.image_name}.raw ${var.output_directory}/${local.image_name}.qcow2",
      "rm ${var.output_directory}/${local.image_name}.raw"
    ]
  }
  # Gerar arquivo de manifesto
  post-processor "manifest" {
    output     = "${var.output_directory}/${local.image_name}-manifest.json"
    strip_path = true
    custom_data = {
      image_name    = local.image_name
      build_date    = timestamp()
      ubuntu_version = var.ubuntu_version
      builder       = "Packer ${packer.version}"
    }
  }
}
Agora, vamos criar o arquivo de variáveis:
1
nano packer/ubuntu/variables.pkr.hcl
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# Variáveis básicas
variable "image_prefix" {
  type    = string
  default = "ubuntu-server"
}
variable "ubuntu_version" {
  type    = string
  default = "24.04"
}
variable "disk_size" {
  type    = string
  default = "20480"
}
variable "memory" {
  type    = string
  default = "2048"
}
variable "cpus" {
  type    = string
  default = "2"
}
# Variáveis de ISO
variable "iso_url" {
  type    = string
  default = "https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso"
}
variable "iso_checksum" {
  type    = string
  default = "sha256:c5ea60d25d64b3e3a47a6171c1dc78c94e29c5e6e8284b0c44b8a1feddc83e75"
}
# Variáveis de SSH
variable "ssh_username" {
  type    = string
  default = "packer"
}
variable "ssh_password" {
  type    = string
  default = "packer"
}
variable "ssh_timeout" {
  type    = string
  default = "60m"
}
# Variáveis de boot
variable "boot_command" {
  type    = list(string)
  default = [
    "c<wait>",
    "linux /casper/vmlinuz --- autoinstall ds=nocloud-net\\;s=http://{{.HTTPIP}}:{{.HTTPPort}}/ ",
    "console=tty1 console=ttyS0<enter><wait>",
    "initrd /casper/initrd<enter><wait>",
    "boot<enter>"
  ]
}
# Variáveis de output
variable "output_directory" {
  type    = string
  default = "output"
}
# Variáveis de provisionamento
variable "provisioning_scripts" {
  type    = list(string)
  default = [
    "../../scripts/common/update.sh",
    "../../scripts/common/install_packages.sh",
    "../../scripts/ubuntu/configure_services.sh",
    "../../scripts/common/security_hardening.sh",
    "../../scripts/common/cleanup.sh"
  ]
}
Vamos criar o arquivo Cloud-init para o Ubuntu:
1
nano packer/ubuntu/http/user-data
Adicione o conteúdo do arquivo Cloud-init que criamos no tutorial 4.
1
touch packer/ubuntu/http/meta-data
Scripts de Provisionamento
Vamos criar alguns scripts de provisionamento básicos:
1
2
mkdir -p scripts/common scripts/ubuntu
nano scripts/common/update.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash -eux
# Detectar o sistema operacional
if [ -f /etc/debian_version ]; then
    # Debian/Ubuntu
    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    apt-get upgrade -y
elif [ -f /etc/redhat-release ]; then
    # RHEL/CentOS/Oracle Linux
    dnf update -y
fi
1
nano scripts/common/install_packages.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash -eux
# Detectar o sistema operacional
if [ -f /etc/debian_version ]; then
    # Debian/Ubuntu
    export DEBIAN_FRONTEND=noninteractive
    apt-get install -y \
        qemu-guest-agent \
        cloud-init \
        curl \
        wget \
        vim \
        htop \
        net-tools
elif [ -f /etc/redhat-release ]; then
    # RHEL/CentOS/Oracle Linux
    dnf install -y \
        qemu-guest-agent \
        cloud-init \
        curl \
        wget \
        vim \
        htop \
        net-tools
fi
1
nano scripts/common/cleanup.sh
Adicione o conteúdo do script de limpeza que criamos nos tutoriais anteriores.
Torne os scripts executáveis:
1
chmod +x scripts/common/*.sh scripts/ubuntu/*.sh
Testes Automatizados
Vamos criar alguns scripts de teste para validar nossas imagens:
1
2
mkdir -p tests
nano tests/boot_test.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/bin/bash
set -e
# Parâmetros
IMAGE_PATH=$1
SSH_PORT=${2:-2222}
SSH_USER=${3:-packer}
SSH_KEY=${4:-""}
if [ -z "$IMAGE_PATH" ]; then
    echo "Uso: $0 <caminho_da_imagem> [porta_ssh] [usuário_ssh] [chave_ssh]"
    exit 1
fi
echo "Iniciando teste de boot para: $IMAGE_PATH"
# Iniciar a VM
if [ -n "$SSH_KEY" ]; then
    SSH_OPTS="-i $SSH_KEY"
else
    SSH_OPTS=""
fi
# Iniciar a VM com QEMU
VM_PID=$(qemu-system-x86_64 \
  -name "Test-VM" \
  -machine type=q35,accel=kvm \
  -cpu host \
  -m 2048 \
  -drive file=$IMAGE_PATH,format=qcow2,if=virtio \
  -net nic,model=virtio \
  -net user,hostfwd=tcp::$SSH_PORT-:22 \
  -display none \
  -daemonize \
  -pidfile /tmp/test_vm.pid | tee /dev/stderr | grep -o '[0-9]*')
echo "VM iniciada com PID: $VM_PID"
# Aguardar a VM iniciar
echo "Aguardando a VM iniciar..."
for i in {1..30}; do
    if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost echo "VM está online" 2>/dev/null; then
        echo "VM iniciou com sucesso após $i tentativas"
        
        # Executar testes básicos
        echo "Verificando sistema operacional..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "cat /etc/os-release"
        
        echo "Verificando versão do kernel..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "uname -a"
        
        echo "Verificando espaço em disco..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "df -h"
        
        echo "Verificando memória..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "free -m"
        
        # Desligar a VM
        echo "Desligando a VM..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo shutdown -h now" || true
        
        # Aguardar a VM desligar
        echo "Aguardando a VM desligar..."
        for j in {1..30}; do
            if ! kill -0 $VM_PID 2>/dev/null; then
                echo "VM desligou com sucesso"
                exit 0
            fi
            sleep 1
        done
        
        echo "Tempo esgotado aguardando a VM desligar, matando o processo..."
        kill -9 $VM_PID || true
        exit 1
    fi
    sleep 10
done
echo "Tempo esgotado aguardando a VM iniciar"
kill -9 $VM_PID || true
exit 1
1
nano tests/services_test.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/bin/bash
set -e
# Parâmetros
IMAGE_PATH=$1
SSH_PORT=${2:-2222}
SSH_USER=${3:-packer}
SSH_KEY=${4:-""}
if [ -z "$IMAGE_PATH" ]; then
    echo "Uso: $0 <caminho_da_imagem> [porta_ssh] [usuário_ssh] [chave_ssh]"
    exit 1
fi
echo "Iniciando teste de serviços para: $IMAGE_PATH"
# Iniciar a VM
if [ -n "$SSH_KEY" ]; then
    SSH_OPTS="-i $SSH_KEY"
else
    SSH_OPTS=""
fi
# Iniciar a VM com QEMU
VM_PID=$(qemu-system-x86_64 \
  -name "Test-VM" \
  -machine type=q35,accel=kvm \
  -cpu host \
  -m 2048 \
  -drive file=$IMAGE_PATH,format=qcow2,if=virtio \
  -net nic,model=virtio \
  -net user,hostfwd=tcp::$SSH_PORT-:22 \
  -display none \
  -daemonize \
  -pidfile /tmp/test_vm.pid | tee /dev/stderr | grep -o '[0-9]*')
echo "VM iniciada com PID: $VM_PID"
# Aguardar a VM iniciar
echo "Aguardando a VM iniciar..."
for i in {1..30}; do
    if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost echo "VM está online" 2>/dev/null; then
        echo "VM iniciou com sucesso após $i tentativas"
        
        # Verificar serviços essenciais
        echo "Verificando serviço SSH..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo systemctl status sshd" || { echo "Falha no serviço SSH"; exit 1; }
        
        echo "Verificando serviço cloud-init..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo systemctl status cloud-init" || { echo "Falha no serviço cloud-init"; exit 1; }
        
        echo "Verificando serviço qemu-guest-agent..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo systemctl status qemu-guest-agent" || { echo "Falha no serviço qemu-guest-agent"; exit 1; }
        
        # Verificar serviços opcionais (se existirem)
        echo "Verificando serviço nginx (se instalado)..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo systemctl status nginx" || echo "Nginx não instalado ou não está em execução"
        
        echo "Verificando serviço php-fpm (se instalado)..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo systemctl status php*-fpm" || echo "PHP-FPM não instalado ou não está em execução"
        
        # Desligar a VM
        echo "Desligando a VM..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo shutdown -h now" || true
        
        # Aguardar a VM desligar
        echo "Aguardando a VM desligar..."
        for j in {1..30}; do
            if ! kill -0 $VM_PID 2>/dev/null; then
                echo "VM desligou com sucesso"
                exit 0
            fi
            sleep 1
        done
        
        echo "Tempo esgotado aguardando a VM desligar, matando o processo..."
        kill -9 $VM_PID || true
        exit 1
    fi
    sleep 10
done
echo "Tempo esgotado aguardando a VM iniciar"
kill -9 $VM_PID || true
exit 1
1
nano tests/security_test.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/bin/bash
set -e
# Parâmetros
IMAGE_PATH=$1
SSH_PORT=${2:-2222}
SSH_USER=${3:-packer}
SSH_KEY=${4:-""}
if [ -z "$IMAGE_PATH" ]; then
    echo "Uso: $0 <caminho_da_imagem> [porta_ssh] [usuário_ssh] [chave_ssh]"
    exit 1
fi
echo "Iniciando teste de segurança para: $IMAGE_PATH"
# Iniciar a VM
if [ -n "$SSH_KEY" ]; then
    SSH_OPTS="-i $SSH_KEY"
else
    SSH_OPTS=""
fi
# Iniciar a VM com QEMU
VM_PID=$(qemu-system-x86_64 \
  -name "Test-VM" \
  -machine type=q35,accel=kvm \
  -cpu host \
  -m 2048 \
  -drive file=$IMAGE_PATH,format=qcow2,if=virtio \
  -net nic,model=virtio \
  -net user,hostfwd=tcp::$SSH_PORT-:22 \
  -display none \
  -daemonize \
  -pidfile /tmp/test_vm.pid | tee /dev/stderr | grep -o '[0-9]*')
echo "VM iniciada com PID: $VM_PID"
# Aguardar a VM iniciar
echo "Aguardando a VM iniciar..."
for i in {1..30}; do
    if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost echo "VM está online" 2>/dev/null; then
        echo "VM iniciou com sucesso após $i tentativas"
        
        # Verificar configurações de SSH
        echo "Verificando configurações de SSH..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo grep PermitRootLogin /etc/ssh/sshd_config" || { echo "Falha ao verificar PermitRootLogin"; exit 1; }
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo grep PasswordAuthentication /etc/ssh/sshd_config" || { echo "Falha ao verificar PasswordAuthentication"; exit 1; }
        
        # Verificar firewall
        echo "Verificando firewall..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo ufw status || sudo firewall-cmd --state || echo 'Nenhum firewall detectado'" || { echo "Falha ao verificar firewall"; exit 1; }
        
        # Verificar atualizações de segurança
        echo "Verificando atualizações de segurança..."
        if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "test -f /etc/debian_version"; then
            ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo apt-get -s upgrade | grep -i security" || echo "Nenhuma atualização de segurança pendente"
        elif ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "test -f /etc/redhat-release"; then
            ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo dnf check-update --security" || echo "Nenhuma atualização de segurança pendente"
        fi
        
        # Verificar configurações de sysctl
        echo "Verificando configurações de sysctl..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo sysctl -a | grep -E 'net.ipv4.conf.all.rp_filter|net.ipv4.conf.all.accept_source_route|net.ipv4.tcp_syncookies'" || { echo "Falha ao verificar configurações de sysctl"; exit 1; }
        
        # Desligar a VM
        echo "Desligando a VM..."
        ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT $SSH_OPTS $SSH_USER@localhost "sudo shutdown -h now" || true
        
        # Aguardar a VM desligar
        echo "Aguardando a VM desligar..."
        for j in {1..30}; do
            if ! kill -0 $VM_PID 2>/dev/null; then
                echo "VM desligou com sucesso"
                exit 0
            fi
            sleep 1
        done
        
        echo "Tempo esgotado aguardando a VM desligar, matando o processo..."
        kill -9 $VM_PID || true
        exit 1
    fi
    sleep 10
done
echo "Tempo esgotado aguardando a VM iniciar"
kill -9 $VM_PID || true
exit 1
Torne os scripts de teste executáveis:
1
chmod +x tests/*.sh
Configurações de CI/CD
Agora, vamos criar as configurações para diferentes plataformas de CI/CD:
GitLab CI
1
2
mkdir -p ci
nano ci/gitlab-ci.yml
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
stages:
  - validate
  - build
  - test
  - publish
variables:
  PACKER_CACHE_DIR: "${CI_PROJECT_DIR}/.packer_cache"
  OUTPUT_DIR: "${CI_PROJECT_DIR}/output"
  PACKER_LOG: "1"
  PACKER_LOG_PATH: "${CI_PROJECT_DIR}/packer.log"
# Job para validar a configuração do Packer
validate:
  stage: validate
  image: hashicorp/packer:1.10.0
  script:
    - cd packer/ubuntu
    - packer init .
    - packer validate -var "output_directory=${OUTPUT_DIR}" .
  tags:
    - docker
# Job para construir a imagem
build:
  stage: build
  image: hashicorp/packer:1.10.0
  script:
    - mkdir -p ${OUTPUT_DIR}
    - cd packer/ubuntu
    - packer init .
    - packer build -var "output_directory=${OUTPUT_DIR}" .
  artifacts:
    paths:
      - output/*.qcow2
      - output/*-manifest.json
      - packer.log
    expire_in: 1 week
  tags:
    - kvm
  only:
    - main
    - tags
  dependencies:
    - validate
# Job para testar a imagem
test:
  stage: test
  image: ubuntu:24.04
  script:
    - apt-get update
    - apt-get install -y qemu-system-x86 openssh-client
    - IMAGE_PATH=$(find ${OUTPUT_DIR} -name "*.qcow2" | head -n 1)
    - ./tests/boot_test.sh ${IMAGE_PATH}
    - ./tests/services_test.sh ${IMAGE_PATH}
    - ./tests/security_test.sh ${IMAGE_PATH}
  tags:
    - kvm
  only:
    - main
    - tags
  dependencies:
    - build
# Job para publicar a imagem
publish:
  stage: publish
  image: alpine:latest
  script:
    - apk add --no-cache curl
    - IMAGE_PATH=$(find ${OUTPUT_DIR} -name "*.qcow2" | head -n 1)
    - IMAGE_NAME=$(basename ${IMAGE_PATH})
    - MANIFEST_PATH=$(find ${OUTPUT_DIR} -name "*-manifest.json" | head -n 1)
    - |
      if [[ -n "${CI_COMMIT_TAG}" ]]; then
        VERSION=${CI_COMMIT_TAG}
      else
        VERSION=${CI_COMMIT_SHORT_SHA}
      fi
    - |
      echo "Publicando imagem ${IMAGE_NAME} com versão ${VERSION}"
      # Aqui você adicionaria comandos para publicar a imagem em um repositório
      # Por exemplo, usando curl para fazer upload para um servidor de artefatos
      # curl -T ${IMAGE_PATH} "https://artifacts.example.com/images/${VERSION}/${IMAGE_NAME}"
      # curl -T ${MANIFEST_PATH} "https://artifacts.example.com/images/${VERSION}/$(basename ${MANIFEST_PATH})"
  tags:
    - docker
  only:
    - main
    - tags
  dependencies:
    - test
GitHub Actions
1
nano ci/github-workflow.yml
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
name: Build and Test Images
on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup Packer
        uses: hashicorp/setup-packer@main
        with:
          version: '1.10.0'
      - name: Validate Packer configuration
        run: |
          cd packer/ubuntu
          packer init .
          packer validate .
  build:
    needs: validate
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup Packer
        uses: hashicorp/setup-packer@main
        with:
          version: '1.10.0'
      - name: Build image
        run: |
          mkdir -p output
          cd packer/ubuntu
          packer init .
          PACKER_LOG=1 PACKER_LOG_PATH=../../packer.log packer build -var "output_directory=../../output" .
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: images
          path: |
            output/*.qcow2
            output/*-manifest.json
            packer.log
  test:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          name: images
          path: output
      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y qemu-system-x86 openssh-client
      - name: Run tests
        run: |
          chmod +x tests/*.sh
          IMAGE_PATH=$(find output -name "*.qcow2" | head -n 1)
          ./tests/boot_test.sh ${IMAGE_PATH}
          ./tests/services_test.sh ${IMAGE_PATH}
          ./tests/security_test.sh ${IMAGE_PATH}
  publish:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          name: images
          path: output
      - name: Set version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
          else
            echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV
          fi
      - name: Publish image
        run: |
          IMAGE_PATH=$(find output -name "*.qcow2" | head -n 1)
          IMAGE_NAME=$(basename ${IMAGE_PATH})
          MANIFEST_PATH=$(find output -name "*-manifest.json" | head -n 1)
          
          echo "Publicando imagem ${IMAGE_NAME} com versão ${VERSION}"
          # Aqui você adicionaria comandos para publicar a imagem em um repositório
          # Por exemplo, usando curl para fazer upload para um servidor de artefatos
          # curl -T ${IMAGE_PATH} "https://artifacts.example.com/images/${VERSION}/${IMAGE_NAME}"
          # curl -T ${MANIFEST_PATH} "https://artifacts.example.com/images/${VERSION}/$(basename ${MANIFEST_PATH})"
Jenkins
1
nano ci/Jenkinsfile
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
pipeline {
    agent {
        label 'kvm'
    }
    
    environment {
        PACKER_CACHE_DIR = "${WORKSPACE}/.packer_cache"
        OUTPUT_DIR = "${WORKSPACE}/output"
        PACKER_LOG = "1"
        PACKER_LOG_PATH = "${WORKSPACE}/packer.log"
    }
    
    stages {
        stage('Validate') {
            agent {
                docker {
                    image 'hashicorp/packer:1.10.0'
                    args '-v ${WORKSPACE}:/workspace'
                }
            }
            steps {
                sh '''
                cd /workspace/packer/ubuntu
                packer init .
                packer validate -var "output_directory=${OUTPUT_DIR}" .
                '''
            }
        }
        
        stage('Build') {
            agent {
                docker {
                    image 'hashicorp/packer:1.10.0'
                    args '-v ${WORKSPACE}:/workspace --privileged'
                }
            }
            steps {
                sh '''
                mkdir -p ${OUTPUT_DIR}
                cd /workspace/packer/ubuntu
                packer init .
                packer build -var "output_directory=${OUTPUT_DIR}" .
                '''
            }
        }
        
        stage('Test') {
            steps {
                sh '''
                IMAGE_PATH=$(find ${OUTPUT_DIR} -name "*.qcow2" | head -n 1)
                chmod +x tests/*.sh
                ./tests/boot_test.sh ${IMAGE_PATH}
                ./tests/services_test.sh ${IMAGE_PATH}
                ./tests/security_test.sh ${IMAGE_PATH}
                '''
            }
        }
        
        stage('Publish') {
            when {
                anyOf {
                    branch 'main'
                    tag '*'
                }
            }
            steps {
                script {
                    def imagePath = sh(script: "find ${OUTPUT_DIR} -name '*.qcow2' | head -n 1", returnStdout: true).trim()
                    def imageName = sh(script: "basename ${imagePath}", returnStdout: true).trim()
                    def manifestPath = sh(script: "find ${OUTPUT_DIR} -name '*-manifest.json' | head -n 1", returnStdout: true).trim()
                    
                    def version = ""
                    if (env.TAG_NAME) {
                        version = env.TAG_NAME
                    } else {
                        version = env.GIT_COMMIT.substring(0, 7)
                    }
                    
                    echo "Publicando imagem ${imageName} com versão ${version}"
                    // Aqui você adicionaria comandos para publicar a imagem em um repositório
                    // Por exemplo, usando curl para fazer upload para um servidor de artefatos
                    // sh "curl -T ${imagePath} https://artifacts.example.com/images/${version}/${imageName}"
                    // sh "curl -T ${manifestPath} https://artifacts.example.com/images/${version}/$(basename ${manifestPath})"
                }
            }
        }
    }
    
    post {
        always {
            archiveArtifacts artifacts: 'output/*.qcow2, output/*-manifest.json, packer.log', allowEmptyArchive: true
        }
    }
}
Técnicas Avançadas de Solução de Problemas
Agora, vamos explorar algumas técnicas avançadas de solução de problemas com o Packer.
1. Depuração com Logs Detalhados
O Packer oferece logs detalhados que podem ajudar a diagnosticar problemas:
1
PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build template.pkr.hcl
Para analisar os logs, procure por padrões específicos:
1
2
3
4
5
6
7
8
# Erros de SSH
grep -i "ssh" packer.log | grep -i "error"
# Erros de QEMU
grep -i "qemu" packer.log | grep -i "error"
# Erros de provisionamento
grep -i "provisioner" packer.log | grep -i "error"
2. Modo de Depuração Interativo
O Packer oferece um modo de depuração interativo que pausa a execução em pontos-chave:
1
PACKER_LOG=1 packer build -debug template.pkr.hcl
No modo de depuração, o Packer pausa antes de cada etapa e aguarda sua confirmação para continuar. Isso permite que você inspecione o estado da VM em cada etapa.
3. Captura de Tela da VM
Para problemas durante a instalação, você pode capturar telas da VM:
1
2
3
4
5
6
7
8
9
10
source "qemu" "debug" {
  # Configurações básicas...
  
  # Habilitar interface gráfica
  headless = false
  
  # Capturar telas em intervalos
  vnc_port_min = 5900
  vnc_port_max = 5900
}
Você também pode usar o VNC para conectar-se à VM durante o build:
1
vncviewer localhost:5900
4. Análise de Falhas de Boot
Para problemas de boot, você pode modificar o boot_command para incluir pausas mais longas:
1
2
3
4
5
6
7
boot_command = [
  "c<wait10>",  # Espera 10 segundos após pressionar 'c'
  "linux /casper/vmlinuz --- autoinstall ds=nocloud-net\\;s=http://{{.HTTPIP}}:{{.HTTPPort}}/ <wait5>",
  "console=tty1 console=ttyS0<enter><wait10>",
  "initrd /casper/initrd<enter><wait10>",
  "boot<enter>"
]
5. Solução de Problemas de Rede
Para problemas de rede, você pode usar configurações de rede alternativas:
1
2
3
4
5
6
7
source "qemu" "debug" {
  # Configurações básicas...
  
  # Usar bridge em vez de user mode networking
  net_device = "virtio-net"
  net_bridge = "virbr0"
}
6. Solução de Problemas de SSH
Para problemas de SSH, você pode aumentar o timeout e habilitar logs detalhados:
1
2
3
4
5
6
7
source "qemu" "debug" {
  # Configurações básicas...
  
  ssh_timeout = "60m"
  ssh_handshake_attempts = 100
  ssh_pty = true
}
7. Solução de Problemas de Provisionamento
Para problemas de provisionamento, você pode usar o provisionador shell-local para depurar:
1
2
3
4
5
6
7
8
provisioner "shell-local" {
  inline = [
    "echo 'Estado atual da VM:'",
    "echo 'SSH disponível na porta: ${build.SSHPort}'",
    "echo 'Pressione Enter para continuar...'"
  ]
  pause_before = "10s"
}
8. Criando um Script de Diagnóstico
Vamos criar um script de diagnóstico que pode ser executado dentro da VM para coletar informações:
1
nano scripts/common/diagnose.sh
Adicione o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/bin/bash -eux
# Criar diretório para diagnóstico
DIAG_DIR="/tmp/packer_diagnose"
mkdir -p $DIAG_DIR
# Informações do sistema
echo "Coletando informações do sistema..."
uname -a > $DIAG_DIR/uname.txt
cat /etc/os-release > $DIAG_DIR/os-release.txt
lsb_release -a > $DIAG_DIR/lsb-release.txt 2>/dev/null || echo "lsb_release não disponível" > $DIAG_DIR/lsb-release.txt
# Informações de hardware
echo "Coletando informações de hardware..."
lscpu > $DIAG_DIR/lscpu.txt
free -m > $DIAG_DIR/memory.txt
df -h > $DIAG_DIR/disk.txt
lsblk -a > $DIAG_DIR/lsblk.txt
lspci > $DIAG_DIR/lspci.txt 2>/dev/null || echo "lspci não disponível" > $DIAG_DIR/lspci.txt
# Informações de rede
echo "Coletando informações de rede..."
ip addr > $DIAG_DIR/ip-addr.txt
ip route > $DIAG_DIR/ip-route.txt
cat /etc/resolv.conf > $DIAG_DIR/resolv.conf.txt
netstat -tulpn > $DIAG_DIR/netstat.txt 2>/dev/null || ss -tulpn > $DIAG_DIR/ss.txt
# Logs do sistema
echo "Coletando logs do sistema..."
journalctl -b -n 1000 > $DIAG_DIR/journalctl.txt 2>/dev/null || echo "journalctl não disponível" > $DIAG_DIR/journalctl.txt
dmesg > $DIAG_DIR/dmesg.txt
if [ -d /var/log ]; then
    find /var/log -type f -name "*.log" -exec cp {} $DIAG_DIR/ \; 2>/dev/null
fi
# Informações de serviços
echo "Coletando informações de serviços..."
systemctl list-units --type=service > $DIAG_DIR/systemctl-services.txt 2>/dev/null || echo "systemctl não disponível" > $DIAG_DIR/systemctl-services.txt
# Informações de pacotes
echo "Coletando informações de pacotes..."
if command -v dpkg > /dev/null; then
    dpkg -l > $DIAG_DIR/dpkg-packages.txt
elif command -v rpm > /dev/null; then
    rpm -qa > $DIAG_DIR/rpm-packages.txt
fi
# Compactar tudo
echo "Compactando arquivos de diagnóstico..."
tar -czf /tmp/packer_diagnose.tar.gz -C /tmp packer_diagnose
echo "Diagnóstico concluído. Arquivo disponível em /tmp/packer_diagnose.tar.gz"
Torne o script executável:
1
chmod +x scripts/common/diagnose.sh
Para usar este script durante o build do Packer:
1
2
3
4
5
6
7
8
9
10
provisioner "shell" {
  script = "scripts/common/diagnose.sh"
  pause_before = "10s"
}
provisioner "file" {
  source = "/tmp/packer_diagnose.tar.gz"
  destination = "packer_diagnose.tar.gz"
  direction = "download"
}
Otimizando Pipelines CI/CD
Vamos explorar algumas técnicas para otimizar pipelines CI/CD para criação de imagens:
1. Caching de ISOs
Para evitar o download repetido de ISOs, configure o cache do Packer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# GitLab CI
cache:
  paths:
    - .packer_cache/
# GitHub Actions
- name: Cache Packer
  uses: actions/cache@v3
  with:
    path: |
      .packer_cache
    key: ${{ runner.os }}-packer-${{ hashFiles('packer/**/*.hcl') }}
    restore-keys: |
      ${{ runner.os }}-packer-
2. Builds Paralelos
Para construir várias imagens em paralelo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# GitLab CI
build-ubuntu:
  stage: build
  script:
    - cd packer/ubuntu
    - packer build .
build-debian:
  stage: build
  script:
    - cd packer/debian
    - packer build .
# GitHub Actions
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu, debian, oracle]
    steps:
      - name: Build image
        run: |
          cd packer/${{ matrix.os }}
          packer build .
3. Builds Incrementais
Para reduzir o tempo de build, você pode usar builds incrementais:
1
2
3
4
5
6
7
8
source "qemu" "incremental" {
  # Configurações básicas...
  
  # Usar uma imagem base existente
  disk_image = true
  iso_url = "https://artifacts.example.com/images/base-ubuntu-24.04.qcow2"
  iso_checksum = "sha256:..."
}
4. Testes Automatizados Eficientes
Para tornar os testes mais eficientes:
1
2
3
4
5
# Executar testes em paralelo
parallel --jobs 3 ./tests/{} ::: boot_test.sh services_test.sh security_test.sh
# Usar timeouts menores
SSH_TIMEOUT=30 ./tests/boot_test.sh image.qcow2
5. Notificações e Relatórios
Para manter a equipe informada sobre o status dos builds:
1
2
3
4
5
6
7
8
9
10
11
# GitLab CI
notify:
  stage: .post
  script:
    - |
      if [ "$CI_JOB_STATUS" == "success" ]; then
        curl -X POST -H 'Content-type: application/json' --data '{"text":"Build bem-sucedido: '$CI_PROJECT_URL'/pipelines/'$CI_PIPELINE_ID'"}' $SLACK_WEBHOOK_URL
      else
        curl -X POST -H 'Content-type: application/json' --data '{"text":"Build falhou: '$CI_PROJECT_URL'/pipelines/'$CI_PIPELINE_ID'"}' $SLACK_WEBHOOK_URL
      fi
  when: always
Versionamento de Imagens
Uma prática importante é versionar suas imagens de forma consistente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
locals {
  timestamp = formatdate("YYYYMMDDhhmmss", timestamp())
  version   = "${var.image_prefix}-${var.os_version}-${local.timestamp}"
}
source "qemu" "template" {
  vm_name = "${local.version}.raw"
  # ...
}
build {
  # ...
  
  post-processor "manifest" {
    output     = "${var.output_directory}/${local.version}-manifest.json"
    strip_path = true
    custom_data = {
      version     = local.version
      build_date  = timestamp()
      git_commit  = "${env("GIT_COMMIT")}"
      git_branch  = "${env("GIT_BRANCH")}"
      builder     = "Packer ${packer.version}"
    }
  }
}
Exercícios Práticos
Para fixar o conhecimento adquirido neste tutorial, tente os seguintes exercícios:
- Configure um pipeline CI/CD completo em uma das plataformas (GitLab CI, GitHub Actions ou Jenkins)
- Implemente testes automatizados adicionais para validar suas imagens
- Crie um script de diagnóstico personalizado para sua distribuição Linux preferida
- Implemente um sistema de versionamento para suas imagens
- Otimize o tempo de build usando técnicas de caching e builds incrementais
Solução de Problemas Comuns em CI/CD
Problemas de Permissão em Ambientes CI/CD
Em ambientes CI/CD, você pode encontrar problemas de permissão ao executar o QEMU:
1
Error: Failed to initialize machine 'source.qemu.template': error initializing machine for driver 'qemu': exec: "qemu-system-x86_64": permission denied
Solução:
- Use a flag --privilegedao executar o container Docker
- Configure o runner para ter acesso ao KVM (/dev/kvm)
Timeouts em Ambientes CI/CD
Os builds podem demorar mais em ambientes CI/CD:
1
Timeout waiting for SSH.
Solução:
- Aumente o valor de ssh_timeout
- Verifique se a VM está iniciando corretamente
- Use o modo de depuração para identificar o problema
Problemas de Rede em Ambientes CI/CD
Problemas de rede podem impedir o acesso à VM:
1
Error waiting for SSH: dial tcp 127.0.0.1:2222: connect: connection refused
Solução:
- Verifique se o redirecionamento de porta está funcionando
- Use net_bridgeem vez denet_devicese disponível
- Verifique se o SSH está instalado e em execução na VM
Conclusão
Neste tutorial final, exploramos a integração do Packer com pipelines CI/CD e técnicas avançadas de solução de problemas. Aprendemos a:
- Configurar pipelines CI/CD em diferentes plataformas (GitLab CI, GitHub Actions e Jenkins)
- Implementar testes automatizados para validar imagens
- Usar técnicas avançadas de depuração e solução de problemas
- Otimizar pipelines para maior eficiência
- Versionar imagens de forma consistente
A automação da criação de imagens com Packer e sua integração em pipelines CI/CD permite criar infraestrutura como código de forma eficiente, consistente e reproduzível. As técnicas de solução de problemas apresentadas ajudam a diagnosticar e resolver problemas rapidamente, garantindo a robustez do processo.
Esperamos que esta série de tutoriais tenha fornecido uma base sólida para você começar a criar suas próprias imagens automatizadas com Packer e QEMU/KVM. Lembre-se de que a prática é essencial para dominar essas ferramentas, então experimente, adapte e expanda os exemplos apresentados para atender às suas necessidades específicas.