Building images for multiple clouds simultaneously with Packer isn’t just about convenience; it’s about realizing that the "immutable infrastructure" dream hinges on your ability to reliably churn out identical, hardened machine images across vastly different cloud providers.

Let’s see this in action. Imagine you have a Packer configuration file, multicloud.pkr.hcl, that looks something like this:

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "azure_location" {
  type    = string
  default = "eastus"
}

variable "gcp_zone" {
  type    = string
  default = "us-central1-a"
}

source "amazon-ebs" "aws_ami" {
  region          = var.aws_region
  instance_type   = "t3.micro"
  ssh_username    = "ec2-user"

  ami_name        = "my-app-ami-{{timestamp}}"

  source_ami_filter {
    filters = {
      name                = "amzn2-ami-hvm-*-x86_64-gp2"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["amazon"]
  }
}

source "azure-arm" "azure_vhd" {
  location            = var.azure_location
  vm_size             = "Standard_DS1_v2"
  ssh_username        = "azureuser"
  image_urn           = "Canonical:0001-com-ubuntu-server-focal:20_04-lts-gen2:latest"

  managed_image_name  = "my-app-vhd-{{timestamp}}"

  managed_image_resource_group_name = "packer-images"
  os_disk_size_gb     = 30
  os_disk_type        = "Standard_LRS"
}

source "googlecompute" "gcp_image" {
  project_id = "my-gcp-project-id"
  zone       = var.gcp_zone
  ssh_username = "packer"

  image_name = "my-app-image-{{timestamp}}"

  source_image_family = "ubuntu-2004-lts"
  machine_type        = "e2-medium"
  disk_size = 30
}

build {
  name = "multicloud-app"
  sources = [
    "source.amazon-ebs.aws_ami",
    "source.azure-arm.azure_vhd",
    "source.googlecompute.gcp_image",
  ]

  provisioner "shell" {
    inline = [
      "sudo apt-get update -y",
      "sudo apt-get install -y nginx",
      "sudo systemctl enable nginx",
      "echo 'Hello from Packer!' | sudo tee /var/www/html/index.html"
    ]
  }
}

When you run packer build multicloud.pkr.hcl, Packer will orchestrate the creation of an AMI in AWS, a managed image in Azure, and a custom image in Google Cloud, all from a single command. It finds the appropriate base images, launches temporary instances, runs your provisioners (like installing Nginx and configuring a simple web page), and then captures the state of those instances as new, ready-to-deploy images.

The core problem this solves is the operational overhead of managing disparate cloud environments. Instead of maintaining separate build pipelines, scripts, and configurations for each cloud, you centralize it. Packer’s source blocks abstract away the provider-specific details – how to specify an AWS AMI versus an Azure URN versus a GCP image family – while the build block defines the common steps (provisioners) that apply universally.

Internally, Packer acts as a translator and orchestrator. For each source block, it invokes the relevant cloud provider’s API to:

  1. Find a base image: Using the filters or URNs you provide.
  2. Launch a temporary instance: With specific machine types and SSH credentials.
  3. Connect via SSH: To provision the instance.
  4. Run provisioners: Executing shell scripts, Ansible playbooks, or other automation.
  5. Stop and capture the instance: Creating a new, reusable image (AMI, VHD, custom image).
  6. Clean up: Terminating the temporary instance and any associated resources.

You control the specifics through the parameters in each source block (e.g., instance_type, vm_size, machine_type, ami_name, managed_image_name, image_name, region, location, zone) and the commands in your provisioner blocks. The {{timestamp}} function is crucial for ensuring unique image names, preventing conflicts.

The real magic is how Packer handles authentication. It uses the standard authentication mechanisms for each cloud provider: AWS credentials (environment variables, ~/.aws/credentials), Azure Service Principal credentials (environment variables like ARM_CLIENT_ID), or GCP service account keys (environment variable GOOGLE_APPLICATION_CREDENTIALS). As long as your environment is configured correctly for each cloud, Packer will authenticate seamlessly.

A subtle but powerful aspect of Packer’s multi-cloud capability is its ability to use variables and functions to dynamically configure builds. Notice the use of var.aws_region, var.azure_location, and var.gcp_zone. This allows you to define common configurations that can be overridden at build time, perhaps to target different regions for different clouds or to parameterize your image builds without altering the core HCL file. You can pass these in using -var 'aws_region=us-west-2' on the command line.

What most people don’t realize is how critical the ssh_username parameter is across providers. While it seems obvious, the default usernames vary: ec2-user for Amazon Linux, ubuntu for Ubuntu, packer for some GCP images, and azureuser for Azure. If you’re building an image based on a distribution that Packer doesn’t have a common default for, or if you’re using a custom base image, you might need to explicitly set this ssh_username to match the user that SSH is enabled for on that specific base image.

The next thing you’ll likely encounter is managing secrets within your provisioners, which requires a separate strategy like using a secrets manager or templating tools.

Want structured learning?

Take the full Packer course →