Packer 镜像构建#
服务器启动时,有两个选择:
- 从基础镜像启动,然后在运行时安装依赖、配置应用
- 从预构建镜像启动,所有依赖已经打包好,开机即用
第一种方式意味着每次部署都要等待包下载、依赖安装、应用配置。10 台服务器同时扩容时,这个过程可能持续 5-10 分钟。
第二种方式呢?Packer 构建的镜像已经包含了所有内容。服务器启动后立即就绪,扩容时间可以缩短到 30 秒。
这就是 Packer 的价值:把构建时间从运行时转移到构建时。
#Packer 是什么
Packer 是 HashiCorp 开发的镜像构建工具,通过声明式配置同时为多个平台构建镜像(AMI、Docker Image、GCP Image 等)。
example.json
{
"builders": [
{
"type": "amazon-ebs",
"region": "us-east-1",
"ami_name": "my-web-app-{{ timestamp }}",
"source_ami": "ami-12345678",
"instance_type": "t3.micro",
"ssh_username": "ubuntu",
"ami_description": "Web application image"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo systemctl enable nginx"
]
}
]
}#核心概念
#Builders
Builder 是 Packer 的核心,负责创建镜像。每个 Builder 对应一个目标平台。
builders.json
{
"builders": [
{
"type": "amazon-ebs",
"access_key": "{{ user `aws_access_key` }}",
"secret_key": "{{ user `aws_secret_key` }}",
"region": "us-east-1",
"source_ami": "ami-ubuntu-22-04",
"ami_name": "my-app-{{ timestamp }}",
"instance_type": "t3.micro",
"ssh_username": "ubuntu",
"ami_users": ["123456789012"],
"ami_product_codes": []
}
]
}#HCL2 格式(推荐)
aws.pkr.hcl
source "amazon-ebs" "web" {
ami_name = "my-web-app-${formatdate("YYYYMMDD", timestamp())}"
instance_type = "t3.micro"
region = "us-east-1"
source_ami = "ami-ubuntu-22-04"
ssh_username = "ubuntu"
tags = {
Name = "my-web-app"
Environment = "production"
BuiltBy = "Packer"
Date = formatdate("YYYY-MM-DD", timestamp())
}
}
build {
name = "web-app"
sources = ["source.amazon-ebs.web"]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx"
]
}
post-processor "manifest" {
output = "manifest.json"
strip_path = true
}
}#Provisioners
Provisioner 负责配置镜像,在构建过程中执行安装和配置任务。
provisioners.pkr.hcl
build {
sources = ["source.amazon-ebs.web"]
# Shell provisioner
provisioner "shell" {
script = "./scripts/install-deps.sh"
}
# Ansible provisioner
provisioner "ansible" {
playbook_file = "./ansible/webserver.yml"
extra_arguments = ["--tags", "nginx"]
}
# File provisioner
provisioner "file" {
source = "./config/nginx.conf"
destination = "/tmp/nginx.conf"
}
# 开机脚本
provisioner "windows-restart" {
restart_timeout = "10m"
}
}#多平台构建
#同时构建多个镜像
multi-platform.pkr.hcl
source "amazon-ebs" "ubuntu" {
ami_name = "my-app-ubuntu-${formatdate("YYYYMMDD", timestamp())}"
source_ami = "ami-ubuntu-22-04"
instance_type = "t3.micro"
region = "us-east-1"
ssh_username = "ubuntu"
}
source "amazon-ebs" "amazon-linux" {
ami_name = "my-app-amazonlinux-${formatdate("YYYYMMDD", timestamp())}"
source_ami = "ami-amazon-linux-latest"
instance_type = "t3.micro"
region = "us-east-1"
ssh_username = "ec2-user"
}
source "googlecompute" "ubuntu" {
project_id = "my-project"
source_image_family = "ubuntu-2204-lts"
zone = "us-east1-b"
image_name = "my-app-${formatdate("YYYYMMDD", timestamp())}"
machine_type = "e2-medium"
ssh_username = "ubuntu"
}
build {
name = "multi-platform"
sources = [
"source.amazon-ebs.ubuntu",
"source.amazon-ebs.amazon-linux",
"source.googlecompute.ubuntu"
]
provisioner "shell" {
inline = ["sudo apt-get update && sudo apt-get install -y nginx"]
}
}#变量使用
variables.pkr.hcl
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "app_version" {
type = string
}
variable "env" {
type = string
default = "prod"
}
locals {
timestamp = formatdate("YYYYMMDD", timestamp())
ami_name = "myapp-${var.env}-${local.timestamp}"
}
source "amazon-ebs" "web" {
ami_name = local.ami_name
instance_type = "t3.micro"
region = var.aws_region
# ...
}#Provisioners 详解
#Shell Provisioner
shell-provisioner.pkr.hcl
provisioner "shell" {
# 内联命令
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx postgresql-client",
"sudo systemctl enable nginx"
]
# 或脚本文件
script = "./scripts/setup.sh"
# 环境变量
env {
APP_ENV = "production"
}
# 执行超时
execute_command = "echo '{{ .Vars }}' | sudo -S bash -c '{{ .Path }}'"
}scripts/setup.sh
#!/bin/bash
set -e
echo "Setting up application..."
# 安装依赖
apt-get update
apt-get install -y nginx postgresql-client curl
# 下载应用
curl -o /opt/app.tar.gz https://example.com/app.tar.gz
tar -xzf /opt/app.tar.gz -C /opt
# 配置应用
cat > /etc/systemd/system/myapp.service <<EOF
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/app
ExecStart=/opt/app/bin/start
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl enable myapp#Ansible Provisioner
ansible-provisioner.pkr.hcl
provisioner "ansible" {
playbook_file = "./ansible/webserver.yml"
extra_arguments = [
"--verbose",
"--tags", "nginx,database"
]
ansible_env_vars = [
"ANSIBLE_ROLES_PATH=./ansible/roles"
]
}ansible/webserver.yml
---
- name: Configure web server
hosts: all
become: yes
roles:
- nginx
- app#Windows Provisioner
windows.pkr.hcl
source "amazon-ebs" "windows" {
ami_name = "my-windows-app-${formatdate("YYYYMMDD", timestamp())}"
instance_type = "t3.medium"
region = "us-east-1"
source_ami = "ami-windows-server-2022"
user_data_file = "./scripts/windows-setup.ps1"
communicator = "winrm"
winrm_username = "Administrator"
winrm_password = "SecretPassword123!"
}
provisioner "powershell" {
inline = [
"Install-WindowsFeature -Name Web-Server",
"Set-Content -Path C:\\inetpub\\wwwroot\\index.html -Value 'Hello from Packer'"
]
}
provisioner "windows-restart" {
restart_timeout = "10m"
}#Post-Processors
#镜像发布
post-processors.pkr.hcl
build {
sources = ["source.amazon-ebs.web"]
# 主要配置...
# 生成 manifest
post-processor "manifest" {
output = "build_manifest.json"
strip_path = true
}
# 压缩镜像
post-processor "compress" {
output = "my-app.tar.gz"
}
# 上传到 Artifactory
post-processor "artifactory" {
api_url = "https://artifactory.example.com"
repo = "packer-images"
os = "linux"
}
# 打标签
post-processor "tag" {
only_cmds = ["docker"]
tag = ["latest", "v1.0.0"]
}
}#触发器
triggers.pkr.hcl
build {
name = "web-app"
sources = ["source.amazon-ebs.web"]
# 只有代码变更时才重新构建
trigger {
name = "app-update"
include = ["app/**/*"]
}
# 代码变更时重新构建数据层
trigger {
name = "data-layer"
include = ["data/**/*"]
exclude = ["app/**/*"]
}
}#CI/CD 集成
#GitHub Actions
.github/workflows/packer.yml
name: Build Packer Images
on:
push:
branches:
- main
paths:
- 'packer/**'
- 'ansible/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Packer
uses: hashicorp/setup-packer@v2
with:
version: "1.10.0"
- name: Run Packer Build
run: |
packer init .
packer validate .
packer build -timestamp-ui .
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Upload Manifest
uses: actions/upload-artifact@v3
with:
name: build-manifest
path: manifest.json#Jenkins
Jenkinsfile
pipeline {
agent any
environment {
AWS_REGION = 'us-east-1'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Validate') {
steps {
sh 'packer init .'
sh 'packer validate .'
}
}
stage('Build') {
steps {
sh '''
packer build \
-var "env=production" \
-timestamp-ui \
.
'''
}
}
stage('Notify') {
steps {
slackSend(
channel: '#infrastructure',
message: "Packer build completed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
}
}
post {
always {
archiveArtifacts artifacts: 'manifest.json'
}
}
}#最佳实践
#1. 镜像分层
flowchart TD
A[Base OS] --> B[Base + Languages]
B --> C[Base + Languages + Frameworks]
C --> D[Base + Languages + Frameworks + App]分层构建
# 基础镜像(共享)
source "amazon-ebs" "base" {
ami_name = "my-base-${formatdate("YYYYMMDD", timestamp())}"
source_ami = "ami-ubuntu-22-04"
instance_type = "t3.micro"
}
build {
name = "base"
sources = ["source.amazon-ebs.base"]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get upgrade -y"
]
}
}
# 应用镜像(基于基础镜像)
source "amazon-ebs" "app" {
ami_name = "my-app-${formatdate("YYYYMMDD", timestamp())}"
source_ami_filter {
filters = {
name = "my-base-*"
tag:BuiltBy = "Packer"
}
most_recent = true
}
instance_type = "t3.micro"
}
build {
name = "app"
sources = ["source.amazon-ebs.app"]
provisioner "shell" {
inline = ["sudo apt-get install -y my-application"]
}
}#2. 安全性
安全加固
source "amazon-ebs" "secure" {
ami_name = "my-secure-app-${formatdate("YYYYMMDD", timestamp())}"
source_ami = "ami-ubuntu-22-04"
instance_type = "t3.micro"
# 加密根卷
root_volume_size = 20
encrypted = true
# SSH 密钥
ssh_keypair_name = "packer-build-key"
ssh_private_key_file = "./keys/packer.pem"
}
provisioner "shell" {
inline = [
# 安全更新
"sudo apt-get update && sudo unattended-upgrades",
# 配置防火墙
"sudo ufw default deny incoming",
"sudo ufw default allow outgoing",
"sudo ufw allow ssh",
"sudo ufw --force enable",
# 禁用 SSH 密码登录
"sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config",
"sudo systemctl restart sshd"
]
}#3. 验证
Testinfra
provisioner "shell" {
inline = [
"pip install testinfra pytest",
"python -m pytest /tmp/tests/"
]
}
provisioner "file" {
content = <<-EOF
import pytest
def test_nginx_installed(host):
nginx = host.package("nginx")
assert nginx.is_installed
def test_nginx_running(host):
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_config(host):
config = host.file("/etc/nginx/nginx.conf")
assert config.exists
def test_app_port_listening(host):
socket = host.socket("8080/tcp")
assert socket.is_listening
EOF
destination = "/tmp/tests/test_app.py"
}#常见问题
#问题一:构建时间长
优化建议
- 使用更小的基础镜像
- 减少不必要的包安装
- 使用 Ansible 的
tags只运行必要的任务 - 考虑分层构建,只重建必要的层 :::
#问题二:构建失败难排查
:::tip 调试方法
- 使用
-debug模式逐步执行 - 配置
ssh_disconnect_script保留环境 - 查看
/var/log/cloud-init-output.log:::
#总结
Packer 的核心价值:
- 构建时间前置:把运行时安装转移到构建时
- 多平台支持:一个配置,多种镜像格式
- 不可变镜像:确保环境一致性
- CI/CD 集成:自动化构建流程
:::info 下一步
想了解不可变镜像的最佳实践?请阅读 镜像不可变最佳实践。