Deploying Packer in GCP to build Windows Images using Cloud Scheduler and Cloud Build with Terraform

When recently working on a customer environment I was looking at ways to improve the startup time of their Windows Application servers. Their sophisticated Powershell startup script was taking about 15–20 minutes to execute fully and involved installing IIS, Azure DevOps & Stackdriver agents and the specialist application software. It was fairly clear to me that time could be saved by packing at least some of these elements into a base image. This then led me to consider deploying Packer on GCP. In this blog post I hope to discuss some of the steps taken to deploy this and how it hangs together.

Step 1: Deploy Packer Builder into GCR

I felt the obvious choice was to run Packer within Cloud Build using a Trigger defined in the Terraform. To make life even easier some kind people in the Cloud Builders Community have already packaged a Packer builder so the first step is simply to push this to the project GCR so Cloud Build can use it. After cloning down that repo change into the packer directory, then simply run:

gcloud builds submit .

Step 2: Writing the Cloud Build Packer job YAML File

So the next step is to define the Cloud Build Packer job using YAML, there is an example provided in the Cloud Builders repo for inspiration but here is an anonymized copy of my resultant file:

- name:${_ENV}/packer
- PROJECT_ID=customer-app-${_ENV}
- build
- -force
- -var
- project_id=customer-app-${_ENV}
- 'packer/app.json'
timeout: 1800s

Step 3: Writing the Packer JSON File

Whilst I appreciate that as of v1.5 Packer can read HCL2, most documentation and examples are still in JSON (including the one I pinched and modified) so I opted to keep it this way:

"builders": [
"type": "googlecompute",
"project_id": "{{user `project_id`}}",
"source_image_family": "windows-2016",
"disk_size": "100",
"disk_type": "pd-ssd",
"machine_type": "n1-standard-2",
"communicator": "winrm",
"subnetwork": "app-vms",
"tags": "packer-winrm",
"winrm_username": "packer_user",
"winrm_insecure": true,
"winrm_use_ssl": true,
"metadata": {
"windows-startup-script-cmd": "winrm quickconfig -quiet & net user /add packer_user & net localgroup administrators packer_user /add & winrm set winrm/config/service/auth @{Basic=\"true\"}"
"zone": "europe-west2-a",
"image_storage_locations": ["europe-west2"],
"image_name": "app-{{timestamp}}",
"image_family": "app-base"
"provisioners": [
"type": "powershell",
"script": "packer/app.ps1"
resource "google_compute_firewall" "packer-winrm" {
name = "packer-winrm"
network =
allow {
protocol = "tcp"
ports = ["5986"]
source_ranges = [""]target_tags = ["packer-winrm"]

Step 4: Writing the Powershell

This step is only applies to Windows but I wanted to mention the requirement to pass a no_shutdown argument to the GCESysprep wrapper to prevent sysprep from restarting the image. Without this argument Packer fails the build as it assumes something has gone wrong.

Write-Host "Installing IIS and all Sub Features"
Install-WindowsFeature -Name Web-Server -IncludeAllSubFeature
# GCESysprep
GCESysprep -no_shutdown

Step 5: Writing Cloud Build Triggers

Normally with Cloud Build you are triggering off a PR or Push to a specific branch of a git repo. However in this case I don’t want to build an image every single time someone pushes to the repo. One solution to this would be to separate Packer into its own repo but in this case it was preferable to keep it in the repo with the infrastructure code. Therefore I created the trigger in a disabled state so it can only be manually triggered:

resource "google_cloudbuild_trigger" "app-packer" {
provider = google-beta
project = var.project_id
name = "app-packer"
description = "Trigger to build Packer Image set to disabled as triggered via Cloud Scheduler"
disabled = true
github {
owner = "customer-org"
name = "customer-app"
push {
branch = var.git_branch
substitutions = {
_ENV = var.env
filename = "packer/packerbuild.yaml"

Step 6: Triggering Cloud Build with Cloud Scheduler

Before I begin this section I want to shout out Neil Kolban’s excellent post about Scheduling Cloud Builds which inspired this section. I won’t go over what he’s already covered but I will provide Terraform code for the Cloud Scheduler job:

resource "google_cloud_scheduler_job" "packer_build" {
name = "packer_build"
description = "Trigger packer cloud build job via HTTP Post"
schedule = "45 23 * * 6"
http_target {
uri = "${var.project_id}/triggers/app-packer:run"
http_method = "POST"
body = base64encode(<<-EOT
"branchName": "master"
oauth_token {
service_account_email =

Step 7: Enjoy Cloud Scheduled Packer Builds :)

Now the Cloud Scheduler job will trigger Cloud Build which will go and package a Windows image using Packer before adding it to an image family from which it can be deployed.

I am a GCP Platform Engineer based in the UK. Thoughts here are my own and don’t necessarily represent my employer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store