Agent Observability Platform: Installation (GCP)
Deploy the Agent Observability data platform on GCP with Terraform
Prerequisites. Complete the Prerequisites first β tooling, project APIs and IAM, the Cloud DNS zone, and Vertex AI Model Garden access.
Overview
The terraform-google-ao-data-platform module provisions the GKE 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 GKE 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, and helm.chart_version.
The examples below install the public artifacts: the Terraform module from the Terraform Registry (
monte-carlo-data/ao-data-platform/google), 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 google, google-beta, kubernetes, and helm providers. The kubernetes and helm providers are wired from the module's outputs β substitute your region for us-central1:
terraform {
required_version = ">= 1.7"
required_providers {
google = { source = "hashicorp/google", version = "~> 7.0" }
google-beta = { source = "hashicorp/google-beta", version = "~> 7.0" }
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.0" }
helm = { source = "hashicorp/helm", version = "~> 2.0" }
}
}
provider "google" {
project = var.project_id
region = "us-central1"
}
provider "google-beta" {
project = var.project_id
region = "us-central1"
}
provider "kubernetes" {
host = module.ao_data_platform.gke_cluster_endpoint
cluster_ca_certificate = module.ao_data_platform.gke_cluster_ca_certificate
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "gke-gcloud-auth-plugin"
}
}
provider "helm" {
kubernetes {
host = module.ao_data_platform.gke_cluster_endpoint
cluster_ca_certificate = module.ao_data_platform.gke_cluster_ca_certificate
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "gke-gcloud-auth-plugin"
}
}
}Two provider notes specific to this module:
google-betais required even though it backs a single resource β provisioning the Secret Manager service agent for CMEK, which the GA provider doesn't expose. Configure it with the same project and region asgoogle.- The
helmprovider must be v2 (~> 2.0) β the module relies on the nestedkubernetes { ... }block syntax, which helm provider v3 changed.
2. Configure the module
The module's location input decides the cluster shape: pass a zone (us-central1-a) for a zonal cluster β the cost-minimal choice β or a region (us-central1) for a regional, HA control plane (node counts multiply across the region's zones). Regional sub-resources always derive their region from it.
New cluster
The module creates the VPC and GKE Standard cluster with sensible defaults (cluster name monte-carlo, private nodes with Cloud NAT egress, Dataplane V2, Workload Identity). A module-created cluster always runs the zoned ClickHouse/Keeper topology β clickhouse_zones and keeper_zones are required (non-empty) on this path. The example below deploys the standard highly available shape β two ClickHouse replicas across two zones, coordinated by a three-voter Keeper ensemble β substitute your region's zone names:
module "ao_data_platform" {
source = "monte-carlo-data/ao-data-platform/google"
version = "~> 1.0"
location = "us-central1-a" # zone β zonal cluster; region β regional control plane
otel_collector_domain = "otel.acme.com"
clickhouse_domain = "clickhouse.acme.com"
dns_zone = "acme-com" # Cloud DNS zone NAME, not the DNS name
gateway = {
letsencrypt_email = "[email protected]" # optional ACME contact
}
# Two ClickHouse replicas across two zones, backed by a 3-voter Keeper ensemble.
clickhouse_zones = ["us-central1-a", "us-central1-b"]
keeper_zones = ["us-central1-a", "us-central1-b", "us-central1-c"]
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-gcp"
}
}
}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, use a single zone with one replica β
clickhouse_zones = ["us-central1-a"],keeper_zones = ["us-central1-a"],clickhouse_replica_count = 1. The zone lists stay required β there is no no-zones layout for a created cluster β see the single-replica variant. A zonal control plane can still run multi-zone node pools, so a zonallocationworks with the HA shape.
Existing cluster
Set cluster.create = false and networking.create_vpc = false (the module rejects a module-created VPC with an existing cluster at plan time), and provide your cluster name, network and subnetwork names, the names of the node subnet's secondary ranges, and the range of your proxy-only subnet:
module "ao_data_platform" {
source = "monte-carlo-data/ao-data-platform/google"
version = "~> 1.0"
location = "us-central1-a" # the existing cluster's zone or region
cluster = {
create = false
existing_cluster_name = "my-cluster"
}
networking = {
create_vpc = false
existing_network_name = "my-vpc"
existing_subnetwork_name = "my-nodes-subnet"
existing_pods_range_name = "pods" # secondary-range NAMES on the node subnet
existing_services_range_name = "services"
proxy_only_cidr = "10.0.16.0/23" # range of the existing proxy-only subnet
}
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-gcp"
}
# 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, including the cluster and VPC prerequisites this path assumes (see also Prerequisites β choose your deployment path).
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 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 makesterraform applyfail to pull the chart. (An absolute local path to a chart directory is also accepted;http(s)://repo URLs are rejected.)
3. Initialize Terraform
terraform initThis downloads the module and the google, google-beta, kubernetes, and helm providers.
4. Review the plan
terraform planReview the planned changes before applying. On the new-cluster path the plan creates a VPC (node subnet with secondary ranges, a proxy-only subnet, Cloud Router + NAT), the GKE cluster and node pools β including one per listed ClickHouse and Keeper zone β Workload Identity IAM bindings, Cloud KMS keys, Secret Manager secrets, DNS IAM grants, and the Helm releases.
Check the zones. Persistent disks are zone-locked, so ClickHouse and Keeper placement is driven by the explicit zone names you list β see ClickHouse and Keeper node pools below. The module verifies at plan time that
clickhouse_zonesandkeeper_zonesare non-empty on the new-cluster path, thatclickhouse_replica_countdoes not exceed the number ofclickhouse_zones, thatkeeper_zoneshas an odd length, and that each list's zones all sit in one region. 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 GCP infrastructure and (with helm.deploy_charts = true) deploys the chart in one pass. Most of the wall-clock time goes to the GKE cluster and node pools; on the HA topology the Helm release itself is allowed up to 15 minutes to converge (Keeper quorum forms, replicas turn Ready, then the schema job runs).
applymodifies your~/.kube/config. To enable the single-pass deploy, the module runsgcloud container clusters get-credentialsinsidelocal-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 |
|---|---|
gke_cluster_name | The cluster name β use it to configure kubectl next |
montecarlo_namespace | The namespace (montecarlo) all components run in |
clickhouse_monte_carlo_credentials_secret_id | Secret Manager secret for the monte_carlo user β the credential you hand to Monte Carlo |
clickhouse_monte_carlo_credentials_secret_version | The same secret's version resource name β pass it directly to gcloud secrets versions access |
clickhouse_otel_credentials_secret_id / clickhouse_schema_owner_credentials_secret_id / clickhouse_llm_worker_credentials_secret_id | Secret IDs for the otel (ingest), 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 |
llm_worker_wif_member | The LLM worker's Workload Identity principal (holds the predict-only Vertex role) |
network_name / subnetwork_name | The platform VPC and node subnet β used when wiring the Monte Carlo Agent |
workload_identity_pool | <project>.svc.id.goog |
clickhouse_node_pools | The dedicated per-zone ClickHouse node pools, keyed by pool name (ch-<zone-suffix>) with each pool's zone and machine type β null when not active (existing cluster or charts not deployed) |
The GCP infrastructure is now provisioned and the chart is deploying. Verifying that the in-cluster components (ClickHouse, the Collector, the LLM worker, the Gateway) 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 persistent disk, so each one runs on its own dedicated single-zone GKE 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-suffix>(one node each, tainteddedicated=clickhouse:NoSchedule, machine typen2-highmem-8by default). - Per-zone Keeper node pools β one per entry in
keeper_zones(one node each, tainteddedicated=keeper:NoSchedule, machine typee2-standard-2by default).
Both zone lists are required on this path β a single-zone list of one entry each is the smallest (dev) shape.
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 (e2-standard-4, autoscaling between 2 and 10 nodes by default).
Persistent disks are zone-locked. Placement is by the explicit zone names in the topology lists β a volume cannot follow a pod to a different zone. If an existing ClickHouse volume lives in a specific zone, include that zone in
clickhouse_zonesso its pool shares the zone.
Node upgrades are yours to schedule. The cluster's
release_channeldefaults toUNSPECIFIEDβ a static cluster where GKE never auto-upgrades (and therefore never auto-drains) the nodes, so routine operation never rolls a stateful ClickHouse or Keeper node implicitly. Kubernetes and node upgrades are customer-managed: pin the control plane withkubernetes_versionand upgrade node pools deliberately. See High availability β node and Kubernetes upgrades.
Harden network access
The Gateway's load balancer IP is private, but by default the module places no restriction on which sources inside your network can reach it. Before relying on the deployment, scope access with gateway.allowed_source_ranges β a single combined allow-list covering both endpoints, implemented as a Cloud Armor policy attached to the Gateway's backends:
null(default) β no module-managed restriction.[]β only the VPC's own ranges (node subnet and pods) may connect.["10.50.0.0/16", ...]β the VPC's ranges plus the listed CIDRs. The VPC ranges are always folded in when a restriction is in effect, so enabling it never locks out in-cluster clients.
If you deploy the Monte Carlo Agent on a dedicated subnet, include that subnet's CIDR in the list. Being an internal load balancer, a listed range is only reachable if a private path into the VPC exists.
Also consider cluster.master_authorized_networks (CIDRs allowed to reach the cluster API endpoint β must include every machine that runs Terraform or kubectl, including CI) and cluster.enable_private_endpoint (removes the public API endpoint entirely). See Network access in the Configuration reference.
Next steps
Continue to Deploy the agent and connect to Monte Carlo.
Updated 1 day ago
