Agent Observability Platform: Installation (Azure)
Deploy the Agent Observability data platform on Azure with Terraform
Configure and apply the Terraform module to deploy the platform.
Prerequisites. Complete the Prerequisites first β tooling, Azure permissions, resource providers, the DNS zone, and the platform artifacts.
Overview
The terraform-azurerm-ao-data-platform module provisions the AKS cluster and, by default (helm.deploy_charts = true), also deploys the ao-data-platform Helm chart β ClickHouse, the OpenTelemetry Collector, and the LLM worker β in the same terraform apply.
The kubernetes and helm providers are configured from the module's outputs, which lets Terraform defer the Kubernetes/Helm resources until after the AKS cluster exists. This is what enables a single-pass apply.
Work through the steps below in order. The required inputs are the same for both cluster paths: location, otel_collector_domain, clickhouse_domain, dns_zone, helm.chart_registry, helm.chart_version, and helm.llm_worker.foundry (the Claude evaluation backend β organization_name is the Azure Marketplace attestation and has no default).
The examples below install the public artifacts: the Terraform module from the Terraform Registry (
monte-carlo-data/ao-data-platform/azurerm), and theao-data-platformHelm chart andao-llm-workerimage from Docker Hub (see Prerequisites). Pulling them requires no registry authentication.
1. Configure the providers
Your root module must configure the azurerm, kubernetes, and helm providers. The kubernetes and helm providers are wired from the module's outputs and authenticate through kubelogin:
terraform {
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.0" }
helm = { source = "hashicorp/helm", version = "~> 2.0" }
}
}
provider "azurerm" {
features {}
}
provider "kubernetes" {
host = module.ao_data_platform.aks_cluster_endpoint
cluster_ca_certificate = module.ao_data_platform.aks_cluster_ca_certificate
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "kubelogin"
args = ["get-token", "--login", "azurecli", "--server-id", "6dae42f8-4368-4678-94ff-3960e28e3630"]
}
}
provider "helm" {
kubernetes {
host = module.ao_data_platform.aks_cluster_endpoint
cluster_ca_certificate = module.ao_data_platform.aks_cluster_ca_certificate
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "kubelogin"
args = ["get-token", "--login", "azurecli", "--server-id", "6dae42f8-4368-4678-94ff-3960e28e3630"]
}
}
}The --server-id value is the well-known, tenant-independent Entra ID application ID of the AKS API server β it is the same for every AKS cluster, so use it verbatim.
Pin the
helmprovider to~> 2.0. Version 3 of the provider changed thekubernetesblock to a top-level argument and is not yet supported β installing the latest provider fails with a configuration-syntax error.
2. Configure the module
New cluster
The module creates the resource group, VNet, and AKS cluster with sensible defaults (cluster name monte-carlo). A module-created cluster always runs the zoned ClickHouse/Keeper topology, so clickhouse_zones and keeper_zones are required on this path. The example below deploys the standard highly available topology β two ClickHouse replicas across two availability zones, coordinated by a three-voter Keeper ensemble:
module "ao_data_platform" {
source = "monte-carlo-data/ao-data-platform/azurerm"
version = "~> 1.0"
location = "eastus"
resource_group_name = "ao-data-platform"
otel_collector_domain = "otel.acme.com"
clickhouse_domain = "clickhouse.acme.com"
dns_zone = "acme.com"
dns_zone_resource_group_name = "dns"
# Two ClickHouse replicas across two zones, backed by a 3-voter Keeper ensemble.
clickhouse_zones = ["1", "2"]
keeper_zones = ["1", "2", "3"]
clickhouse_replica_count = 2
helm = {
chart_registry = "oci://registry-1.docker.io/montecarlodata"
chart_version = "4.0.0"
llm_worker = {
image_tag = "1.1.0-azure"
foundry = {
# Required β the Azure Marketplace attestation for the Anthropic offer.
organization_name = "Acme Inc."
# account_name = "acme-ao-foundry" # a GLOBALLY unique subdomain; defaults to "<cluster name>-foundry"
}
}
}
}See examples/new_cluster/ for a complete, copy-paste starting point including the provider blocks.
Dev/testing: for an environment where zone-failure tolerance isn't worth the node count, run a single replica with a single Keeper voter β see the single-replica variant. Zone entries are Azure availability-zone numbers (
"1","2","3"); a single AKS node subnet is regional, so no per-zone subnets are needed. Also overridenetworking.vnet_address_spaceβ the default10.18.0.0/16is a placeholder.
Existing cluster
Set cluster.create = false and networking.create_vnet = false, and provide the cluster's name and resource group, plus the VNet and the AKS node subnet (entry [0]):
module "ao_data_platform" {
source = "monte-carlo-data/ao-data-platform/azurerm"
version = "~> 1.0"
location = "eastus" # must match the existing cluster's region
cluster = {
create = false
existing_cluster_name = "my-cluster"
existing_resource_group_name = "my-cluster-rg"
}
networking = {
create_vnet = false
existing_vnet_id = "/subscriptions/.../virtualNetworks/my-vnet"
existing_subnet_ids = [
"/subscriptions/.../subnets/aks", # [0] AKS node subnet (required)
]
}
otel_collector_domain = "otel.acme.com"
clickhouse_domain = "clickhouse.acme.com"
dns_zone = "acme.com"
helm = {
chart_registry = "oci://registry-1.docker.io/montecarlodata"
chart_version = "4.0.0"
llm_worker = {
image_tag = "1.1.0-azure"
foundry = {
organization_name = "Acme Inc."
}
}
# Set any controller already present in the cluster to false:
# install_cert_manager = false
# install_external_secrets_operator = false
# install_external_dns = false
}
}See examples/existing_cluster/ for the full configuration. The cluster must already have its OIDC issuer, Workload Identity, and the Gateway API / app-routing-Istio add-ons enabled β the module verifies the add-ons at plan time and fails with a pointer to the az aks update commands if they are missing (see Prerequisites).
On an existing cluster the module does not create the ClickHouse and Keeper node pools (and rejects
clickhouse_zones/keeper_zonesat plan time) β you attach tainted per-zone node pools yourself and take the self-managed Helm route (helm.deploy_charts = false) to wire the scheduling values, since the module-managed release doesn't expose them. See High availability β existing-cluster deployments.
helm.chart_registryis the registry prefix only βoci://registry-1.docker.io/montecarlodata, with no chart name. The module appends/ao-data-platformitself, so adding it here (.../montecarlodata/ao-data-platform) makesterraform applyfail to pull the chart. The artifacts table in Prerequisites lists the full chart path because that's the chart's location β but as a module input, pass only the prefix.
3. Initialize Terraform
terraform initThis downloads the module and the azurerm, kubernetes, and helm providers (plus the module's internal providers, including azapi).
4. Review the plan
terraform planReview the planned changes before applying. On the new-cluster path the plan creates the resource group, VNet with a NAT gateway (stable egress IP), the AKS cluster and node pools β including one per listed ClickHouse and Keeper zone β user-assigned managed identities with federated credentials, a Key Vault with the ClickHouse credential secrets and the etcd encryption key, the Foundry (Claude) evaluation account with its model deployments and a custom least-privilege inference role, and the Helm releases.
Check the availability zones. Azure managed disks are zone-locked, so ClickHouse and Keeper placement is driven by the explicit zone numbers you listed β see ClickHouse and Keeper node pools below. The module verifies at plan time that
clickhouse_zonesandkeeper_zonesare non-empty, thatclickhouse_replica_countdoes not exceed the length ofclickhouse_zones, and thatkeeper_zoneshas an odd length. If the plan fails one of these preconditions, fix the zone lists rather than overriding the guard.
5. Apply
terraform applyReview the plan once more, then confirm. The apply provisions the Azure infrastructure and (with helm.deploy_charts = true) deploys the chart in one pass. Expect a long apply on the new-cluster path: most of it is the AKS cluster and node pools, several deliberate pauses (30β60 seconds each) for Azure RBAC propagation, and the Helm release itself (the HA shape takes about 6β8 minutes inside a single Helm wait; the release timeout is 900 seconds).
applymodifies your~/.kube/config. To enable the single-pass deploy, the module runsaz aks get-credentialsandkubelogin convert-kubeconfiginsidelocal-execprovisioners β it has tokubectl waitfor the External Secrets Operator CRDs and apply aClusterSecretStore, which the native Terraformkubernetes/helmproviders can't do for resources created in the same apply. This adds or refreshes the cluster's context in the~/.kube/configof the machine running Terraform and makes it the current context. It happens on every apply, on both cluster paths.
To manage the Helm release yourself instead, set
helm.deploy_charts = falseand follow the self-managed Helm install.
6. Confirm what was created
terraform outputYou should see (among others):
| Output | What it is |
|---|---|
aks_cluster_name | The cluster name β use it with az aks get-credentials next |
montecarlo_namespace | The namespace (montecarlo) all components run in |
key_vault_uri | The Key Vault holding the ClickHouse credentials |
clickhouse_monte_carlo_credentials_secret_id | Key Vault secret ID for the monte_carlo user β the credential you hand to Monte Carlo |
clickhouse_otel_credentials_secret_id | Secret ID for the otel ingest user (OpenTelemetry Collector) |
clickhouse_schema_owner_credentials_secret_id / clickhouse_llm_worker_credentials_secret_id | Secret IDs for the schema_owner (migrations / MV owner) and llm_worker users |
clickhouse_admin_credentials_secret_id / clickhouse_readonly_user_credentials_secret_id | Secret IDs for the optional admin and readonly_user β null when those users are disabled |
oidc_issuer_url | The cluster's OIDC issuer β used to federate Workload Identities (for example, the Monte Carlo Agent's) |
llm_worker_identity_client_id | The LLM worker's Workload Identity client ID |
foundry_account_name | The Foundry (Claude) evaluation account β also its globally unique subdomain |
clickhouse_node_pools | The dedicated per-zone ClickHouse node pools, keyed by pool name (ch<zone>) β { zone, vm_size } β verify the resolved zones; null on the existing-cluster path or when charts are off |
The Azure infrastructure is now provisioned and the chart is deploying. Verifying that the in-cluster components (ClickHouse, the Collector, the LLM worker) came up healthy is the first step of Deploy the agent and connect to Monte Carlo.
ClickHouse and Keeper node pools
Every ClickHouse replica and every Keeper voter is a stateful pod backed by a zone-locked Azure managed disk, so each one runs on its own dedicated single-zone AKS node pool. On the new-cluster path (with helm.deploy_charts = true) the module creates them all automatically:
- Per-zone ClickHouse node pools β one per entry in
clickhouse_zones(namedch<zone>, one node each, tainteddedicated=clickhouse:NoSchedule, VM sizeStandard_E8s_v5by default). - Per-zone Keeper node pools β one per entry in
keeper_zones(namedkeeper<zone>, one node each, tainteddedicated=keeper:NoSchedule, VM sizeStandard_D2as_v4by default).
Both zone lists are required on this path β there is no single-node layout; a single zone in each list is the low-cost dev floor.
The module wires the matching nodeSelector/tolerations into the Helm release, so the ClickHouse and Keeper pods target their node pools exclusively β no manual configuration required. The OpenTelemetry Collector, LLM worker, and cluster controllers run on the main node pool. Node images are pinned by default (node_os_upgrade_channel = "None"), so routine applies never roll the stateful nodes implicitly β see High availability β node-image updates.
Managed disks are zone-locked. Placement is by the explicit zone numbers in the topology lists β a volume cannot follow a pod to a different zone.
clickhouse_replica_countmay not exceed the number of listed ClickHouse zones (enforced at plan time), and both lists must name zones your region and subscription actually offer.
On the existing-cluster path, the module does not manage these node pools β attach tainted per-zone node pools yourself and take the self-managed Helm route (helm.deploy_charts = false) to pass the matching scheduling values, since the module-managed release doesn't expose them. See High availability β existing-cluster deployments.
Harden network access
Both endpoints are served by a single managed Gateway on an internal load balancer β they are never on the public internet, and clients reach them over private connectivity (VNet, peering, or VPN). Two further controls are worth setting before you rely on the deployment:
- Gateway source ranges.
gateway.allowed_source_rangesrestricts which sources can reach the Gateway's load balancer:null(the default) applies no restriction,[]restricts to the VNet address space, and a CIDR list allows the VNet space plus the listed ranges β the VNet space is always folded in so a restriction can't lock out in-VNet clients. On a bring-your-own VNet the module doesn't know your address space β include it in the list explicitly. See Network access. - AKS API endpoint. By default a new cluster's API server is reachable from anywhere. Set
cluster.api_server_authorized_ip_rangesto allow-list source ranges, orcluster.private_cluster_enabled = truefor a fully private control plane (setting both is rejected). Account for every machine that talks to the API βterraform applyitself needs it (providers and provisioners), and so does CI: hosted runners without stable egress IPs can't be allow-listed, and a private cluster needs a private path (VPN, peering, private endpoint, or an in-VNet runner) for every apply andkubectlinvocation.
Egress from module-created VNets is routed through a NAT gateway with a static public IP, so outbound traffic (Docker Hub pulls, Let's Encrypt, the Foundry endpoint) presents a stable source address you can allow-list elsewhere.
Next steps
Continue to Deploy the agent and connect to Monte Carlo.
Updated 1 day ago
