Skip to Content

Using Packer to Create Docker Windows Image for Azure

I need to have Docker installed on Windows Server 2019. There doesn’t appear to be one that exists in the Azure Marketplace that I could find. Bootstrapping the installation of Docker during VM creation takes some time and requires a reboot as part of the process. There are few solutions to accomplish that, so I decided to create a shared image using Packer. Before we can dive into the Packer configuration, we need to create a resource group and shared image gallery. If you have existing ones that you would like to use, then substitute them.

Pre-Configuration

Here is the Terraform for creating the resource group, the shared image gallery, and the shared image.

terraform {
  required_version = "~> 0.15"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 2.56"
    }
  }
}

provider "azurerm" {
    features {}
}

resource "azurerm_resource_group" "shared_gallery" {
  name     = "shared-gallery-rg"
  location = "East US"
}

resource "azurerm_shared_image_gallery" "shared_gallery" {
  name                = "sharedimagegallery"
  resource_group_name = azurerm_resource_group.shared_gallery.name
  location            = azurerm_resource_group.shared_gallery.location
  description         = "Shared images for Windows with Docker."
}

resource "azurerm_shared_image" "shared_image" {
  name                = "WindowsDocker"
  gallery_name        = azurerm_shared_image_gallery.shared_gallery.name
  resource_group_name = azurerm_resource_group.shared_gallery.name
  location            = azurerm_resource_group.shared_gallery.location
  os_type             = "Windows"

  identifier {
    publisher = "MyPublisher"
    offer     = "WindowsDocker"
    sku       = "2019"
  }
}

Packer Azure Configuration

Now we can start with our Packer configuration. I will be using the HCL version now, and I am glad that the support is improved. I will be using the Azure CLI authentication option, which you can read about here.

variable "subscription_id" {
  description = "Subscription ID to use."
  type        = string
  default     = "00000000-0000-0000-0000-00000000000"
}

source "azure-arm" "docker" {
  use_azure_cli_auth = true
  
  shared_image_gallery_destination {
    subscription        = var.subscription_id
    resource_group      = "shared-gallery-rg"
    gallery_name        = "sharedimagegallery"
    image_name          = "WindowsDocker"
    image_version       = "0.1.0"
    replication_regions = ["East US"]
  }
  managed_image_name                = "Windows2019Docker"
  managed_image_resource_group_name = "shared-gallery-rg"

  os_type         = "Windows"
  image_publisher = "MicrosoftWindowsServer"
  image_offer     = "WindowsServer"
  image_sku       = "2019-Datacenter"

  communicator   = "winrm"
  winrm_use_ssl  = true
  winrm_insecure = true
  winrm_timeout  = "3m"
  winrm_username = "packer"
  
  location = "East US"
  vm_size  = "Standard_A1_v2"
}

Now we need to set up our provisioners. The first one will install Docker using the PowerShell provider. The next one restarts the machine for good measure, and the last one executes the sysprep step.

build {
  sources = ["sources.azure-arm.docker"]

  provisioner "powershell" {
    pause_before = "5m"
    inline = [
      "Write-Host 'Starting Docker installation...'",
      "Write-Host 'Installing NuGet Provider...'",
      "Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force",     
      "Write-Host 'Installing Docker Provider...'",
      "Install-Module -Name DockerMsftProvider -AllowClobber -Confirm:$false -Force",
      "Write-Host 'Installing Package...'",
      "Install-Package -Name docker -ProviderName DockerMsftProvider -Confirm:$false -Force",
      "Write-Host 'Completed Docker installation...'"
    ]
  }

  provisioner "windows-restart" {
    pause_before = "1m"
  }  
  
  provisioner "powershell" {
     pause_before = "3m"
     inline = [
          "while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
          "while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
          "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit /mode:vm",
          "while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10  } else { break } }"
     ]
  }
}

Now we can save this all as docker-windows.pkr.hcl and execute it.

The build

We need to log in using the Azure CLI.

$ az login

Now we can initialize Packer.

$ packer init

Now execute the build, and don’t forget to either set your subscription variable to your subscription or pass it in on the command line. The build should take about 30 minutes with the VM size I have entered. A larger VM will be quicker.

$ packer build docker-windows.pkr.hcl

Once that is complete, you should see a version in your image gallery.

Conclusion

Now you can use this new image as the base image for creating virtual machines in Azure that you require to have Docker installed. This effort will become helpful for container-based workflows, which I will hopefully demonstrate in a later post.

Thanks for reading,

Jamie

If you enjoy the content then consider buying me a coffee.