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
--privileged
ao 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_bridge
em vez denet_device
se 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.