Azure Monitor Private Link Scope (AMPLS) is a very convenient Azure service that amalgamates the various services required for Azure Monitor and makes them available via private endpoints within your virtual network (VNet). This enables completely private interactions with all aspects of the Azure Monitor service, from sending logs to workspaces and storage accounts to running queries. It also allows you to connect on-premises infrastructure to Azure Monitor over ExpressRoute. These features make AMPLS particularly attractive to enterprise and regulated consumers of Azure.

ℹ️  Tip
AMPLS is not the focus of this article. To find out more about AMPLS and some of the potential gotchas, have a look at the Microsoft documentation.

If you try setting up AMPLS through the Azure portal, say for a proof of concept or to become familiar with the service, you’ll find the experience quite straightforward. It’s simple to set up the private link service and then add the private endpoints and DNS configuration required to use it. However, if you then start looking in to deploying this solution with Terraform, things become a bit more difficult. This article offers simple solutions to overcome the challenges you’ll face.

Hurdle Number One: Provider Support

The first issue is that at the time of writing (July 2021) the Terraform Azure provider does not support AMPLS. The main reason for this is that the feature is not yet supported by the Azure Go SDK. Once the feature exists in the Go SDK it can then be added to the Terraform provider.

ℹ️  Tip
You can follow the GitHub issue for AMPLS support in the azurerm provider here.

This means we’ll need to use good old ARM templates to deploy AMPLS, the tried and true fall-back when something is not yet supported by the azurerm provider. If you haven’t run in to this scenario before then you’ll be pleased to know that the provider does offer a mechanism to deploy ARM templates, so you can still execute the deployment as part of your Terraform infrastructure code. Here’s the code to do this.

resource "azurerm_resource_group_template_deployment" "this" {
  name                = "ampls"
  resource_group_name = azurerm_resource_group.example.name
  deployment_mode     = "Incremental"
  parameters_content  = jsonencode({
    "private_link_scope_name" = {
      value = "ampls_private_scope"
    }
    "workspace_name" = {
      value = azurerm_log_analytics_workspace.this.name
    }
  })
  template_content = <<-EOT
    {
      "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "private_link_scope_name": {
          "defaultValue": "my-scope",
          "type": "String"
        },
        "workspace_name": {
          "defaultValue": "my-workspace",
          "type": "String"
        }
      },
      "variables": {},
      "resources": [
        {
          "type": "microsoft.insights/privatelinkscopes",
          "apiVersion": "2019-10-17-preview",
          "name": "[parameters('private_link_scope_name')]",
          "location": "global",
          "properties": {}
        },
        {
          "type": "microsoft.insights/privatelinkscopes/scopedresources",
          "apiVersion": "2019-10-17-preview",
          "name": "[concat(parameters('private_link_scope_name'), '/', concat(parameters('workspace_name'), '-connection'))]",
          "dependsOn": [
            "[resourceId('microsoft.insights/privatelinkscopes', parameters('private_link_scope_name'))]"
          ],
          "properties": {
            "linkedResourceId": "[resourceId('microsoft.operationalinsights/workspaces', parameters('workspace_name'))]"
          }
        }
      ],
      "outputs": {
        "resourceID": {
          "type": "String",
          "value": "[resourceId('microsoft.insights/privatelinkscopes', parameters('private_link_scope_name'))]"
        }
      }
    }
  EOT
}

Hurdle Number Two: Azure DNS Records

The second hurdle to be considered is DNS. There are two parts to this, the architectural problem and the infrastructure code problem. We’re going to focus on the latter here, but just be aware the you should put careful consideration in to how you configure DNS for AMPLS in the context of your environment. The Microsoft documentation on AMPLS covers these potential gotchas well.

The infrastructure code problem we face when trying to deploy AMPLS with Terraform is around replicating the outcome of deploying the service from the Azure portal. If you use the portal and create a private endpoint from the private link scope resource, the portal will offer an option to deploy the Azure Private DNS zones for you.

When you are creating the private endpoint and linking it to the private link scope in Terraform (or any infrastructure as code) this won’t be done automatically for you. You’ll need to create the DNS zones and A records yourself. The important thing to note here is that for the private endpoint to work properly you will need to assign particular IPs for each FQDN. The private endpoint will list the IP to be assigned to each FQDN if you take a look in the portal.

What’s happening here is that IPs from the private endpoint’s subnet are being allocated to each FQDN in a certain order. If we replicate that order we’ll be able to dynamically assign IPs from any give subnet in the same way the service would have if deployed via the portal. That means our AMPLS will work as expected and we’ll be able to deploy using automation and infrastructure as code.

We’ll need to use the Terraform cidrhost function which returns the IP address for a given host number within a subnet. So for example if we want the first host IP in the subnet 192.168.0.0/24:

cidrhost("192.168.0.0/24", 1)

From here we just need to know what order the IPs need to be allocated in to replicate the outcome of deploying via the portal so that each IP is assigned to the correct FQDN. Keep in mind that the first four IP addresses from every subnet are reserved in Azure. You’ll also need the workspace ID of the log analytics workspace you are sending your logs to. Note that this is the workspace ID which is different to the resource ID. With that in mind, this is the order in which the IPs need to be allocated.

NumberFQDN
4<workspace id>.privatelink.oms.opinsights.azure.com
5<workspace id>.privatelink.ods.opinsights.azure.com
6<workspace id>.privatelink.agentsvc.azure-automation.net
7api.privatelink.monitor.azure.com
8global.in.ai.privatelink.monitor.azure.com
9profiler.privatelink.monitor.azure.com
10live.privatelink.monitor.azure.com
11snapshot.privatelink.monitor.azure.com
12scadvisorcontentpl.privatelink.blob.core.windows.net

With that resolved you’ll either need to assign the private DNS zones to any VNets that need to consume them, or update your DNS resolvers to forward requests for these zones to Azure DNS depending on your environment.

ℹ️  Tip
The full Terraform code to deploy a demo of the AMPLS solution including AMPLS, private endpoints and DNS is available here. Note that this code is purely for demonstration only and is not production ready. You'll need to apply the solution to the context of your environment and extend it to meet your needs.

Conclusion

Now you’re ready to leverage the benefits of keeping all your monitoring traffic on private links and deploy it all using Terraform infrastructure as code. We’ve dealt with the lack of provider support with an ARM template deployment and resolved the DNS allocation issue as well. With the technical issues dealt with hopefully this frees you up to put more thought in to how AMPLS will fit in to your environment.