This is a script I used to spin up temporary VMs for testing Docker Toolbox.

The script requires Terraform to run (of course), and Azure CLI & AWS CLI for authentication. The state file is stored on Amazon S3 (in a bucket I use for all state files), and Route 53 is used for DNS – just because that’s what I already had set up.

It creates a Standard_D2_v3 VM (2 vCPU, 8 GiB RAM, 50GB SSD temporary storage, supports nested virtualisation) in the North Europe (Ireland) region. The cost is currently $0.199/hour (which works out to $145/month – so don’t leave it running too long!).

It also installs Google Chrome, mainly so I wouldn’t have to use Internet Explorer to download other software.

Usage

Create the two scripts below then run:

# Configure AWS credentials (in ~/.aws/credentials)
aws configure

# Configure Azure credentials
az login
# Then each time the access token expires (about every 2 hours) run
az account get-access-token

# Install Terraform modules
terraform init

# Create VM
terraform apply

# Delete VM
terraform destroy

Scripts

main.tf

# Store state on S3 so it can be shared between different computers
terraform {
  backend "s3" {
    bucket  = "davejamesmiller-terraform"
    key     = "azure-windows2016.tfstate"
    region  = "eu-west-1"
  }
}

provider "aws" {
  # Credentials are read from ~/.aws/credentials (use "aws configure")
  region  = "eu-west-1"
}

resource "azurerm_resource_group" "windows2016" {
  name     = "Windows2016ResourceGroup"
  location = "North Europe" # Ireland
}

resource "azurerm_virtual_network" "windows2016" {
  name                = "Windows2016VirtualNetwork"
  address_space       = ["10.0.0.0/16"]
  location            = "${azurerm_resource_group.windows2016.location}"
  resource_group_name = "${azurerm_resource_group.windows2016.name}"
}

resource "azurerm_subnet" "windows2016" {
  name                 = "Windows2016Subnet"
  resource_group_name  = "${azurerm_resource_group.windows2016.name}"
  virtual_network_name = "${azurerm_virtual_network.windows2016.name}"
  address_prefix       = "10.0.2.0/24"
}

resource "azurerm_public_ip" "windows2016" {
  name                         = "Windows2016PublicIP"
  location                     = "${azurerm_resource_group.windows2016.location}"
  resource_group_name          = "${azurerm_resource_group.windows2016.name}"
  public_ip_address_allocation = "static"
}

resource "azurerm_network_interface" "windows2016" {
  name                = "Windows2016NetworkInterface"
  location            = "${azurerm_resource_group.windows2016.location}"
  resource_group_name = "${azurerm_resource_group.windows2016.name}"

  ip_configuration {
    name                          = "Windows2016IPConfig"
    subnet_id                     = "${azurerm_subnet.windows2016.id}"
    public_ip_address_id          = "${azurerm_public_ip.windows2016.id}"
    private_ip_address_allocation = "dynamic"
  }
}

# WARNING: This stores the randomly generated password unencrypted in the state file (on S3)
resource "random_string" "windows2016_password" {
  # To change the password, either destroy + apply, or "terraform taint random_string.windows2016_password"
  length = 16
  special = true
}

resource "azurerm_virtual_machine" "windows2016" {
  name                             = "Windows2016VM"
  location                         = "${azurerm_resource_group.windows2016.location}"
  resource_group_name              = "${azurerm_resource_group.windows2016.name}"
  network_interface_ids            = ["${azurerm_network_interface.windows2016.id}"]
  vm_size                          = "Standard_D2_v3"
  delete_os_disk_on_termination    = true
  delete_data_disks_on_termination = true

  storage_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2016-Datacenter"
    version   = "latest"
  }

  storage_os_disk {
    name              = "Windows2016OSDisk"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }

  os_profile {
    computer_name  = "windows2016"
    admin_username = "Dave"
    admin_password = "${random_string.windows2016_password.result}"
  }

  os_profile_windows_config {
    provision_vm_agent        = true
    enable_automatic_upgrades = true
  }
}

# Install Chrome
resource "azurerm_storage_account" "windows2016" {
  name                     = "davejamesmillerwin2016"
  resource_group_name      = "${azurerm_resource_group.windows2016.name}"
  location                 = "${azurerm_resource_group.windows2016.location}"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "extensions" {
  name                  = "extensions"
  resource_group_name   = "${azurerm_resource_group.windows2016.name}"
  storage_account_name  = "${azurerm_storage_account.windows2016.name}"
  container_access_type = "blob"
}

resource "azurerm_storage_blob" "installchrome" {
  name                   = "InstallChrome.ps1"
  resource_group_name    = "${azurerm_resource_group.windows2016.name}"
  storage_account_name   = "${azurerm_storage_account.windows2016.name}"
  storage_container_name = "${azurerm_storage_container.extensions.name}"
  type                   = "block"
  source                 = "InstallChrome.ps1"
}

resource "azurerm_virtual_machine_extension" "windows2016" {
  name                       = "Windows2016Extensions-${md5(azurerm_virtual_machine.windows2016.id)}" # ID forces recreation on taint
  location                   = "${azurerm_resource_group.windows2016.location}"
  resource_group_name        = "${azurerm_resource_group.windows2016.name}"
  virtual_machine_name       = "${azurerm_virtual_machine.windows2016.name}"
  publisher                  = "Microsoft.Compute"
  type                       = "CustomScriptExtension"
  type_handler_version       = "1.9"
  auto_upgrade_minor_version = true

  settings = <<END
    {
      "fileUris": [
        "${azurerm_storage_blob.installchrome.url}"
      ],
      "commandToExecute": "powershell -ExecutionPolicy Unrestricted -File ${azurerm_storage_blob.installchrome.name}"
    }
  END
}

# Set up a DNS record so I can save the password
data "aws_route53_zone" "djm" {
  name = "djm.me."
}

resource "aws_route53_record" "windows2016" {
  zone_id = "${data.aws_route53_zone.djm.zone_id}"
  name    = "azure-windows2016.${data.aws_route53_zone.djm.name}"
  type    = "A"
  ttl     = "30"
  records = ["${azurerm_public_ip.windows2016.ip_address}"]
}

output "ipv4" {
  value = "${azurerm_public_ip.windows2016.ip_address}"
}

output "hostname" {
  value = "${replace(aws_route53_record.windows2016.name, "/\\.$/", "")}"
}

output "rdp_command" {
  value = "mstsc /v:${replace(aws_route53_record.windows2016.name, "/\\.$/", "")} /f"
}

output "username" {
  value = "Dave"
}

output "password" {
  value = "${random_string.windows2016_password.result}"
}

InstallChrome.ps1

# Install Google Chrome - because who wants to deal with IE Protected Mode!
# https://www.google.com/chrome/
$Source = "https://dl.google.com/tag/s/appguid%3D%7B8A69D345-D564-463C-AFF1-A69D9E530F96%7D%26iid%3D%7B81AC3877-4B45-1A78-C7CB-F31FFCF2C333%7D%26lang%3Den%26browser%3D4%26usagestats%3D1%26appname%3DGoogle%2520Chrome%26needsadmin%3Dprefers%26ap%3Dx64-stable-statsdef_1%26installdataindex%3Ddefaultbrowser/update2/installers/ChromeSetup.exe"
$TempDir = "c:\Temp"
$TempFile = "$TempDir\ChromeSetup.exe"

New-Item -ItemType Directory -Force -Path $TempDir
Invoke-WebRequest $Source -OutFile $TempFile
Start-Process -FilePath $TempFile -Args "/silent /install" -Verb RunAs -Wait