Packer can generate a "golden image" of your infrastructure, and Chef can define that infrastructure’s desired state.
Let’s say you want to build a standard Ubuntu 22.04 AMI for your AWS environment, pre-configured with Nginx and a specific nginx.conf. Packer will orchestrate the build, and Chef will handle the configuration.
Here’s a Packer template (ubuntu-nginx.pkr.hcl) to get us started:
source "amazon-ebs" "ubuntu" {
ami_name = "ubuntu-nginx-{{timestamp}}"
instance_type = "t3.micro"
region = "us-east-1"
source_ami_filter {
filters = {
Name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
Root-Device-Type = "ebs"
}
most_recent = true
owners = ["099720109477"] # Canonical's owner ID
}
ssh_username = "ubuntu"
}
build {
sources = ["source.amazon-ebs.ubuntu"]
provisioner "chef-client" {
config_dir = "chef-config"
run_list = ["recipe[nginx::default]"]
}
}
This template tells Packer to:
- Use an
amazon-ebsbuilder. - Name the resulting AMI
ubuntu-nginx-followed by a timestamp. - Use a
t3.microinstance type for the build. - Specify the
us-east-1region. - Find the most recent Ubuntu 22.04 (Jammy) LTS AMI from Canonical.
- Connect to the instance using the
ubuntuuser. - Crucially, use the
chef-clientprovisioner. - Point to a
chef-configdirectory. - Execute a Chef run list that includes
recipe[nginx::default].
Now, we need to define that nginx::default recipe. Create a chef-config directory next to your Packer template. Inside chef-config, create a cookbooks directory. Inside cookbooks, create an nginx directory. And inside nginx, create a recipes directory.
Your chef-config/cookbooks/nginx/recipes/default.rb would look like this:
package 'nginx' do
action :install
end
service 'nginx' do
supports status: true
action [:enable, :start]
end
cookbook_file '/etc/nginx/nginx.conf' do
source 'nginx.conf' # This will look for nginx.conf in the cookbook's files/default directory
owner 'root'
group 'root'
mode '0644'
notifies :reload, 'service[nginx]', :delayed
end
This Chef recipe will:
- Install the
nginxpackage. - Ensure the
nginxservice is enabled and started. - Copy a custom
nginx.conffile into place.
For the cookbook_file resource to work, you need to create a files/default directory inside your chef-config/cookbooks/nginx directory. Place your custom nginx.conf file there.
# Example custom nginx.conf
# chef-config/cookbooks/nginx/files/default/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
To run this, you’d execute:
packer init .
packer build .
Packer will launch an EC2 instance, bootstrap it with Chef, run the specified Chef recipes, and then create an AMI from that instance. The chef-client provisioner handles downloading and running Chef on the temporary build instance. Packer automatically uploads your chef-config directory to the instance for Chef to use.
The most surprising truth is that Packer’s chef-client provisioner doesn’t require a full Chef Server or even Chef Solo to be pre-installed on the base AMI. Packer handles bootstrapping the instance with a minimal Chef client that can then fetch and run your cookbook code, treating your local chef-config directory as its "cookbooks path" for the duration of the build. It effectively makes your local cookbooks available to the temporary build instance.
After your AMI is built, the next step is to launch EC2 instances from it and verify that Nginx is running and configured correctly, perhaps by testing the custom nginx.conf with curl.