Why Packer with Terraform Matters
Building servers on-demand is convenient, but provisioning them during every deployment slows you down and introduces variability. Packer with Terraform solves this by baking your application stack into machine images beforehand, so Terraform can launch pre-configured instances in seconds. This practical Terraform & IaC guide shows you how to combine image-as-code with infrastructure-as-code for faster, more reliable deployments.
Packer Bakes AMIs with Your Software
Packer is a tool that automates machine image creation. Instead of manually installing dependencies, configuring services, and snapshotting instances, you define the image once in code. Packer builds that image—whether it's an AWS AMI, Azure image, or GCP image—and you version it like any other artifact.
The typical flow: Packer boots a temporary instance, runs provisioners (shell scripts, Ansible, Chef) to install your runtime, application dependencies, monitoring agents, and security patches, then snapshots the instance into an image. The temporary instance is destroyed, leaving you with a reusable image.
Here's a simple Packer template for building an AMI with Node.js and your application:
packer {
required_plugins {
amazon = {
version = ">= 1.0"
source = "github.com/hashicorp/amazon"
}
}
}
variable "app_version" {
type = string
default = "1.0.0"
}
source "amazon-ebs" "web" {
ami_name = "my-app-${var.app_version}"
instance_type = "t3.micro"
region = "us-east-1"
source_ami_filter {
filters = {
name = "amzn2-ami-hvm-*-x86_64-gp2"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["137112412989"]
}
tags = {
Name = "my-app"
Version = var.app_version
}
}
build {
sources = ["source.amazon-ebs.web"]
provisioner "shell" {
inline = [
"sudo yum update -y",
"sudo yum install -y nodejs npm",
"sudo yum install -y cloudwatch-agent"
]
}
provisioner "file" {
source = "./app"
destination = "/tmp/app"
}
provisioner "shell" {
inline = [
"cd /tmp/app",
"npm install --production",
"sudo cp -r /tmp/app /opt/myapp",
"sudo chown -R ec2-user:ec2-user /opt/myapp"
]
}
provisioner "shell" {
script = "./scripts/configure-systemd.sh"
}
post-processor "manifest" {
output = "manifest.json"
strip_path = true
}
}Run packer build -var app_version=1.2.3 packer.hcl, and Packer outputs an AMI ID. That ID becomes your deployable artifact. You can store it in a manifest file, push it to a registry, or pass it directly to Terraform.
Terraform Launches Them at Scale
Once Packer produces an AMI, Terraform references it to launch instances. Instead of Terraform running provisioners on every instance (which is slow and error-prone), it simply uses the pre-baked image. Your infrastructure code becomes declarative and fast.
Here's how Terraform consumes a Packer-built image:
locals {
app_version = "1.2.3"
}
data "aws_ami" "app" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["my-app-${local.app_version}"]
}
}
resource "aws_launch_template" "app" {
name_prefix = "app-"
image_id = data.aws_ami.app.id
instance_type = "t3.small"
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "app-instance"
Version = local.app_version
}
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "app" {
name = "app-asg"
min_size = 2
max_size = 10
desired_capacity = 3
vpc_zone_identifier = var.subnet_ids
health_check_type = "ELB"
health_check_grace_period = 300
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "app-asg-instance"
propagate_at_launch = true
}
lifecycle {
create_before_destroy = true
}
}The data.aws_ami block queries your AWS account for the most recent Packer-built image matching the name. Terraform then uses that AMI ID in the launch template. When the ASG scales up or instances terminate, Terraform launches new ones from the same image. No drift, no inconsistency.
Faster, More Reliable Boots
The performance gain is substantial. Without Packer, Terraform would provision each instance:
- Update OS packages (1-3 minutes)
- Install runtime and dependencies (2-5 minutes)
- Copy application code (1-2 minutes)
- Run database migrations (highly variable)
With Packer, the image already contains the OS, runtime, and dependencies. Terraform's boot time drops to under a minute, and instances are ready to serve traffic immediately.
Reliability also improves. Packer bakes the image once, testing it thoroughly before it reaches production. Terraform then uses that tested image repeatedly. You eliminate the variability of provisioning different instances at different times with potentially different network conditions or package versions.
In high-traffic scenarios, this matters. During a traffic spike, your ASG scales up and instances boot in 30–60 seconds instead of 10 minutes. Your service handles the load without degradation.
Version Images Like Code
Treat Packer images as versioned artifacts, just like application builds. Use semantic versioning or a timestamp-based scheme.
# Packer: tag and version your image
tags = {
Name = "my-app"
Version = "1.2.3"
BuildTime = timestamp()
GitCommit = var.git_commit
Environment = var.environment
}# Terraform: reference a specific version
locals {
image_version = "1.2.3"
}
data "aws_ami" "app" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["my-app-${local.image_version}"]
}
filter {
name = "tag:Environment"
values = [var.environment]
}
}This approach enables rollbacks. If version 1.2.3 has issues, revert local.image_version to 1.2.2, run terraform apply, and your ASG starts launching instances from the previous image. The entire deployment is reversible and auditable.
Integrating Packer and Terraform in CI/CD
A typical CI/CD pipeline:
- Build stage: Compile application, run tests, package into Docker image or artifact.
- Packer stage: Run
packer buildwith the application version. Output the AMI ID to a file (manifest). - Terraform stage: Read the AMI ID from the manifest, update Terraform variables or locals, run
terraform planandterraform apply. - Verification: Run smoke tests against the new instances.
Here's a shell snippet to orchestrate this:
#!/bin/bash
set -e
APP_VERSION=$(git describe --tags --always)
echo "Building Packer image for version $APP_VERSION..."
packer build \
-var "app_version=$APP_VERSION" \
-var "git_commit=$(git rev-parse HEAD)" \
packer.hcl
echo "Extracting AMI ID from manifest..."
AMI_ID=$(jq -r '.builds[0].artifact_id' manifest.json | cut -d':' -f2)
echo "Deploying with Terraform..."
terraform plan \
-var "app_version=$APP_VERSION" \
-var "image_id=$AMI_ID" \
-out=tfplan
terraform apply tfplan
echo "Deployment complete. AMI: $AMI_ID"This ties Packer's output directly to Terraform's input. The entire flow is reproducible and version-controlled.
Common Mistakes
Baking too much into images: If your image is 5GB and contains every possible dependency, builds take forever and storage costs climb. Bake essentials—runtime, security patches, monitoring agents, application code. Keep heavy dependencies or configuration in Terraform-managed services (RDS, ElastiCache, S3).
Not versioning images: Without tagging and versioning, you can't distinguish which image is running where. Always tag Packer outputs with version, build time, and git commit. This enables auditing and rollbacks.
Mixing provisioners and image-building: If you use both Packer provisioners and Terraform provisioners, you create confusion. Decide: bake it into the image or provision it at runtime. Mixing both makes debugging harder and reintroduces the variability Packer is meant to solve.
Ignoring image decay: An image built six months ago may have security vulnerabilities. Rebuild images regularly (weekly or monthly) with the latest patches, even if application code hasn't changed. Treat image maintenance as infrastructure hygiene.
Not testing images before deployment: Run smoke tests on Packer-built images before Terraform uses them in production. Validate that services start, health checks pass, and logging is configured.
Key Takeaways
- Packer automates machine image creation, baking your runtime, dependencies, and application code into a reusable artifact.
- Terraform references Packer-built images via
data.aws_amifilters, eliminating runtime provisioning and reducing boot times to seconds. - Version images like code using semantic versioning, git commits, and metadata tags for auditability and easy rollbacks.
- Integrate Packer and Terraform in CI/CD pipelines so application builds flow directly into infrastructure deployments.
- Avoid baking too much, mixing provisioning strategies, or letting images decay. Keep images lean, consistent, and regularly refreshed.
- This pattern scales: whether you're deploying two instances or two thousand, the image is identical and reliable.






Comments
Loading comments…