The good people at Hashicorp have recently merged a couple of my pull requests into the Terraform AzureRM provider to enable support for Managed Service Identity

To recap, MSI allows an Azure virtual machine to retrieve an access token for the Azure API so it can manage resources.

So what cool things can we do with this? Here’s an example of creating a VM to use as a Terraform CI build server.

I’m going to use the buildkite agent, which is a neat little CI tool that can be deployed in a container. To get the agent up and running I’ll deploy a CoreOS VM and configure it to run the buildkite container as a service.

data "ignition_systemd_unit" "systemd_unit" {
  name    = "bkagent.service"
  enabled = true
  content = "${file("${path.module}/bkagent.service")}"
}

data "ignition_config" "config" {
  systemd = [
    "${data.ignition_systemd_unit.systemd_unit.id}",
  ]
}

resource "azurerm_virtual_machine" "virtual_machine" {
  name                  = "coreos"
  location              = "${azurerm_resource_group.resource_group.location}"
  resource_group_name   = "${azurerm_resource_group.resource_group.name}"
  network_interface_ids = ["${azurerm_network_interface.network_interface.id}"]
  vm_size               = "Standard_DS1_v2"

  identity = {
    type = "SystemAssigned"
  }

  storage_image_reference {
    publisher = "CoreOS"
    offer     = "CoreOS"
    sku       = "Stable"
    version   = "latest"
  }

  storage_os_disk {
    name              = "coreos"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Premium_LRS"
  }

  os_profile {
    computer_name  = "coreos"
    admin_username = "azureuser"
    admin_password = "${var.password}"

    custom_data = "${data.ignition_config.config.rendered}"
  }

  os_profile_linux_config {
    disable_password_authentication = false
  }
}

resource "azurerm_virtual_machine_extension" "virtual_machine_extension" {
  name                 = "coreos"
  location             = "${azurerm_resource_group.resource_group.location}"
  resource_group_name  = "${azurerm_resource_group.resource_group.name}"
  virtual_machine_name = "${azurerm_virtual_machine.virtual_machine.name}"
  publisher            = "Microsoft.ManagedIdentity"
  type                 = "ManagedIdentityExtensionForLinux"
  type_handler_version = "1.0"

  settings = <<SETTINGS
    {
        "port": 50342
    }
SETTINGS
}

data "azurerm_subscription" "subscription" {}

data "azurerm_builtin_role_definition" "builtin_role_definition" {
  name = "Contributor"
}

resource "azurerm_role_assignment" "role_assignment" {
  scope              = "${data.azurerm_subscription.subscription.id}"
  role_definition_id = "${data.azurerm_subscription.subscription.id}${data.azurerm_builtin_role_definition.builtin_role_definition.id}"
  principal_id       = "${lookup(azurerm_virtual_machine.virtual_machine.identity[0], "principal_id")}"
}

To enable MSI we set the identity property on the VM to SystemAssigned and then apply the ManagedIdentityExtensionForLinux VM extension.

The principal_id is then retrieved from the VM and used to create a new role_assignment. For this example we just give the VM contributor rights to the subscription it is in.

To get the buildkite agent running the CoreOS Ignition resource is used to set up a systemd unit to run the agent in a container. The bkagent.service file looks like this:

[Unit]
Description=Buildkite Agent Container
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker kill bkagent
ExecStartPre=-/usr/bin/docker rm bkagent
ExecStart=/usr/bin/docker run --name bkagent --network=host -e BUILDKITE_AGENT_TOKEN=<REPLACE WITH TOKEN> buildkite/agent:3

[Install]
WantedBy=multi-user.target

This will give us a buildkite agent connected to our account. To run Terraform on the agent set some environment variables in the pipeline.yml file:

env:
  ARM_USE_MSI: true
  ARM_MSI_ENDPOINT: http://localhost:50342/oauth2/token
  ARM_TENANT_ID: <my tenant>
  ARM_SUBSCRIPTION_ID: <my subscription>

You can now run Terraform on this agent and it will automatically log in using the VM’s managed service identity. You could hand the agent over to a dev team to use for deployments without needing to configure or provide credentials for them - everything is handled entirely through Terraform and abstracted away. The same approach will work for VSTS, or Jenkins, or any other agent based CI tool.

You could also run the Azure CLI on the agent using az login --msi or write your own code to call the MSI endpoint and get a token for the API - there’s all sorts of interesting possibilities.