Terraform variable reference#

Inputs and outputs of the common materialize-monitoring Terraform module.

Most deployments do not set these directly — a per-cloud wrapper in materialize-terraform-self-managed forwards the ones that matter and supplies the storage and identity inputs itself. See Installing via Terraform for the variables you actually set in a terraform.tfvars.

Inputs#

VariableTypeDescriptionDefault / Schema
additional_valueslist(string)Raw YAML documents appended to the Helm values, in order, after everything this module computes. Later documents win, so anything here overrides the module's opinion.

This is the supported way to reach chart settings the module does not model — including scheduling (node selectors, tolerations) and Grafana ingress, neither of which the module surfaces yet. See the README.

Each element is one YAML document. .tfvars is HCL, so an indented heredoc is the readable way to write one — <<-YAML strips the common leading indent, and ${...} escapes a literal $ ahead of a brace. The marker is arbitrary; YAML rather than the usual EOT only because this description is itself a heredoc:

additional_values = [
  <<-YAML
    grafana:
      ingress:
        enabled: true
        hosts: ["grafana.example.com"]
  YAML
]
[]
certificate_durationstringLifetime of each issued certificate, as a Go duration (e.g. `2160h`). Null keeps the chart's default of 90 days.

Keep certificate_renew_before well under this — a third or less. cert-manager renews at duration - renewBefore, so a value close to duration renews continuously; on a small cluster that has been observed to livelock the controller, after which it stops renewing and reports certificates as healthy while they expire.

&{}
certificate_renew_beforestringHow long before expiry cert-manager renews, as a Go duration (e.g. `720h`). Null keeps the chart's default of 30 days. See the warning on `certificate_duration`.&{}
certificates_enabledboolRender cert-manager `Certificate` resources for in-cluster TLS.

Requires cert-manager to already be installed, with its CRDs present. This module does not install it — the same shared-responsibility split as buckets and workload identity — so with the flag on and cert-manager absent the apply fails on an unknown cert-manager.io/v1 kind.

Off by default rather than on. The design calls for the Terraform path to be secure by default, and that becomes safe once a wrapper that installs cert-manager owns the default; flipping it here today would break every existing consumer’s next apply.

Issuing certificates does not turn TLS on anywhere — var.internal_tls is what moves the hops off plaintext, and it needs this. The two are separate because a component only leaves plaintext once its renewal behaviour is proven, so the material exists before every hop is ready to use it.

false
chart_registrystringOCI registry holding the materialize-monitoring charts. Override for a mirrored or air-gapped registry.oci://ghcr.io/materializeinc/helm-charts
chart_versionstringVersion of the materialize-monitoring chart.

Leave null, which is the supported path: the module reads the version out of the chart’s own Chart.yaml in this repository, so a module ref always installs the chart it shipped with and the two cannot drift. Set it only to pin a chart version different from the module’s.

&{}
crds_chart_versionstringVersion of the materialize-monitoring-crds chart. Read from its `Chart.yaml` when null, like `chart_version`. Tracked separately because the CRDs chart has a deliberately looser lifecycle.&{}
create_namespaceboolWhether this module creates the namespace. Defaults to false because the Materialize operator module already creates `monitoring` in the supported topology.false
datadog_api_keystringDatadog API key for `datadog_metrics`.

Delivered as a Secret this module creates (mzmon-alloy-gateway-env), never through the Helm values — anything in values is readable with helm get values by anyone who can read the release Secret. The gateway reads it from the environment at startup.

An app key is not needed and is not accepted here; the metrics intake authenticates with the API key alone.

&{}
datadog_metricsschemaAlso export metrics to Datadog from the Alloy gateway. Null disables it; Thanos is unaffected either way. Pair it with `datadog_api_key`, which is what actually authenticates.

site is your Datadog site — datadoghq.com, datadoghq.eu, us3.datadoghq.com, and so on. Getting it wrong is a 403 from the intake, not a routing error.

min_importance picks a metric tier — essential, recommended, extended, diagnostic, or all — and each tier includes the ones below it. This is a cost control, and a sharper one than for most backends: Datadog bills per custom metric, so all is rarely what you want.

metric_endpoint and logs_endpoint override the intake URLs the exporter derives from site. Leave them null unless you are routing through a proxy or PrivateLink — a hand-written endpoint that disagrees with site fails at the intake rather than at plan time.

object({
    site            = optional(string, "datadoghq.com")
    min_importance  = optional(string, "essential")
    metric_endpoint = optional(string)
    logs_endpoint   = optional(string)
  })
enable_monitoring_crdsboolInstall the materialize-monitoring-crds chart (prometheus-operator and grafana-operator CRDs).

Set false when the cluster already has them from elsewhere — kube-prometheus-stack, or a platform team that owns CRDs centrally — since Terraform would otherwise fail trying to create objects it does not own.

Note the teardown blast radius: destroying this release deletes the CRDs, which cascades to every GrafanaDashboard, GrafanaDatasource, PrometheusRule, and PodMonitor in the cluster, including ones this stack did not create. It is a separate helm_release so it can be targeted independently (-target=module.monitoring.helm_release.crds).

The Grafana custom resources still have to go before grafana-operator does, or their finalizers have no remover and the CRDs wedge in Terminating. The chart’s pre-delete hook handles that ordering now — but it lives in the main release, so destroying this one first takes the resource types out from under it. Destroy in the module’s own order, and see the “Uninstalling” page in the docs.

true
enable_sql_scraperboolEnable the SQL-on-scrape collector against environmentd.

On by default. The chart currently works with an empty password.

true
gateway_service_account_annotationsmap(string)Annotations for the Alloy gateway's ServiceAccount, for binding it to a cloud identity — `eks.amazonaws.com/role-arn` for IRSA, `iam.gke.io/gcp-service-account` for Workload Identity.

Required by a sigv4 remote-write destination, which has no other source of credentials. Merged with any annotations object_storage contributes, so both can be present.

map[]
google_cloud_metricsschemaAlso export metrics to Google Cloud Monitoring from the Alloy gateway. Null disables it; Thanos is unaffected either way.

min_importance picks a metric tier — essential, recommended, extended, diagnostic, or all — and each tier includes the ones below it. This is a cost control: GCM bills per custom metric and all sends the entire surface.

Authentication is ADC only. Bind the gateway ServiceAccount to a Google service account holding roles/monitoring.metricWriter through object_storage.gateway_service_account_annotations; failing that it falls back to the node’s service account, which works only if that account has the role.

object({
    min_importance = optional(string, "recommended")
    prefix         = optional(string)
  })
grafana_admin_passwordstringGrafana admin password. Generated when null. Supplied to Grafana as a Secret this module owns, rather than letting the bundled chart mint one — the chart's own generation does not survive upgrades.&{}
grafana_admin_userstringGrafana admin username.admin
grafana_database_enabledboolWhether to point Grafana at PostgreSQL at all.

Null infers it from grafana_database_host, which is right whenever that host is a literal. Set it explicitly when the host is computed from a resource created in the same apply.

&{}
grafana_database_hoststringHostname of the PostgreSQL database backing Grafana's own state. Null (the default) leaves Grafana on SQLite, where everything created through the UI is lost on every restart. Host only — the port is `grafana_database_port`.&{}
grafana_database_manage_password_secretboolWhether this module creates the Secret holding the database password, and references it from `grafana.ini` with `$__file{}`.

Null infers it from grafana_database_password. Set it explicitly when that password is generated in the same apply.

False is not a way to supply the password by another route: it means no Secret and no $__file{} reference at all, so a connection that needs one has to get both from additional_values. That is also the shape for a genuinely passwordless connection — a Cloud SQL Auth Proxy sidecar, or peer authentication.

&{}
grafana_database_namestringName of the database Grafana owns.grafana
grafana_database_passwordstringPassword for `grafana_database_user`, supplied to Grafana as a Secret this module owns and read from a mounted file rather than the environment.

Never inlined into grafana.ini, which renders into a ConfigMap.

Null when the connection needs no password — a Cloud SQL Auth Proxy sidecar with --auto-iam-authn, or a trust/peer-authenticated database. Note that IAM database authentication without a proxy does not work: Grafana reads its password once at startup and has no refresh hook, so the first reconnect after the token expires fails.

&{}
grafana_database_portnumberPort for `grafana_database_host`.5432
grafana_database_ssl_modestringlibpq SSL mode for the Grafana database connection.

require encrypts but does not authenticate the server. verify-full also authenticates it and is the better choice — but it needs a CA bundle on disk, which this module does not mount: supply grafana.ini.database.ca_cert_path and the matching grafana.extraSecretMounts through additional_values when you use it.

require
grafana_database_userstringDatabase user Grafana connects as. Must own `grafana_database_name`, because Grafana runs schema migrations at startup.grafana
grafana_external_dns_nameslist(string)Public DNS names to put on a browser-facing certificate for Grafana, issued from `var.issuer_ref`.

Only needed behind an L4 load balancer, which passes TCP through and leaves TLS to terminate at the pod, so the material has to exist in the cluster. An L7 load balancer terminating with a cloud-managed certificate (ACM, Google Certificate Manager, Azure Key Vault) attaches it by ARN or resource ID and the key never enters the cluster — for that shape leave this empty and pass the annotation through grafana.service.annotations in additional_values.

Setting this with no var.issuer_ref issues nothing, and the render says so.

[]
install_metrics_serverboolInstall metrics-server as part of this stack.

Leave false when the Materialize operator module installs it (the default topology), and set it true when that module has install_metrics_server = false — otherwise nothing provides the metrics API and the Materialize Console silently loses cluster metrics.

false
install_node_exporterboolInstall node-exporter as part of this stack.

On by default: node-level metrics are part of the stack’s baseline, and nothing else in it collects them. Set false when the cluster already runs its own node-exporter DaemonSet — a second one wastes a per-node slot and produces the same series twice under two job labels, which double-counts in any sum() over them.

This writes the chart’s node-exporter.enabled circuit breaker rather than a tag. Tags are OR’d, so tags.node-exporter = false would not turn it off while tags.default is true.

true
install_timeoutnumberTimeout for each Helm release, in seconds. Well above Helm's 300s default: a first install brings up Loki, Thanos, Grafana, and both Alloy roles together.900
internal_issuer_refschemaOptional override for the issuer signing cluster-internal certificates (those carrying `*.svc.` SANs).

Required when var.issuer_ref points at a public ACME issuer such as Let’s Encrypt, since a public CA cannot sign in-cluster names. When null, var.issuer_ref is used for both.

Under the chart’s split-namespace layout this must be a ClusterIssuer: a namespaced Issuer signs only for its own namespace, and the components are spread across several. The chart refuses that combination at render time.

object({
    name = string
    kind = string
    # Defaulted rather than required, because every in-tree issuer is
    # `cert-manager.io` and making callers restate it would be noise. It has to
    # exist, though: an external issuer — AWS Private CA
    # (`awspca.cert-manager.io`), Google CAS (`cas-issuer.jetstack.io`) — lives
    # in its own API group, and cert-manager resolves `issuerRef` by group as
    # well as kind. Hardcoding the default group made those unreachable through
    # Terraform even though the chart has always taken the field.
    group = optional(string, "cert-manager.io")
  })
internal_tlsstringHow far the in-cluster hops move off plaintext. Requires `certificates_enabled`, which is what issues the material these settings point at.

The stages are the chart’s own profiles/mtls*.values.yaml, composed in order, and they exist because Kubernetes does not order a server’s rollout against its clients’. On a fresh install there is nothing to strand, so go straight to authenticate. On a running stack step through present first, or on any hop where the server pod happens to roll before the writers, ingestion stops until they catch up — which on a busy pipeline is data loss rather than latency.

ValueProfilesState
offnoneplaintext everywhere
encryptmtlsservers serve TLS, clients verify them; no client certificates anywhere
present+ mtls-phase2clients also present a certificate; servers still serve anyone. A way-station, not a destination — nothing is rejected on the Thanos hop, and only a wrong-CA certificate is on the Loki one
authenticate+ mtls-phase3servers require a client certificate from their CA

authenticate is authentication, not authorization: none of these components can express “this identity may write and that one may not”, so the size of the trust domain is the security property. That is the argument for leaving issuer_ref null and letting the chart bootstrap a root scoped to this release.

Two hops stop short of authenticate by nature rather than by choice, and the chart refuses to configure them otherwise: Loki’s HTTP port is probed by the kubelet, and a httpGet probe has no field for a client certificate, so that hop’s terminal state is present. Grafana’s datasource verifies the backend but presents nothing.

off
issuer_refschemaDefault cert-manager (Cluster)Issuer used for the monitoring stack's TLS certificates. Used for both the external (browser-facing) certificate and the internal ones unless overridden by `var.internal_issuer_ref`.

Leave null with certificates_enabled on and the chart bootstraps a self-signed root of its own, scoped to this release — which is the recommended shape rather than a fallback. None of the receiving components here implement per-client authorization, so “signed by the CA we trust” is the entire authorization decision; an issuer shared with every workload in the cluster reduces that to “has any certificate”.

object({
    name = string
    kind = string
    # Defaulted rather than required, because every in-tree issuer is
    # `cert-manager.io` and making callers restate it would be noise. It has to
    # exist, though: an external issuer — AWS Private CA
    # (`awspca.cert-manager.io`), Google CAS (`cas-issuer.jetstack.io`) — lives
    # in its own API group, and cert-manager resolves `issuerRef` by group as
    # well as kind. Hardcoding the default group made those unreachable through
    # Terraform even though the chart has always taken the field.
    group = optional(string, "cert-manager.io")
  })
materialize_instance_namespacestringNamespace the Materialize instance runs in. Used to scope scrape targets.materialize-environment
materialize_operator_namespacestringNamespace the Materialize operator runs in.materialize
min_zonesnumberNumber of availability zones the node pool can actually launch in, used to adjust the hard zone spread on Thanos Receive and Loki's ingesters. Null leaves the chart's defaults alone, which assume two or more zones and is correct for every managed cloud default.

Set this when that assumption does not hold, because the chart’s constraints fail closed rather than degrading:

  • 0 — no node carries a topology.kubernetes.io/zone label. Common on kind and on many on-premises distributions. The hard constraints are dropped; the soft host spread stays.
  • 1 — a single zone. minDomains becomes 1, which is satisfiable now and turns into real protection the day a second zone appears.
  • 2 or more — minDomains is set to the real count, which is stricter than the chart’s floor of 2 whenever you have more zones than that.

Leaving this null on a cluster with fewer than two zones leaves those pods Pending forever rather than unbalanced: below minDomains Kubernetes treats the global minimum as 0, so one zone holding every replica computes a skew equal to the replica count. One zone is exactly as broken as none.

&{}
namespacestringNamespace to install the monitoring stack into.monitoring
node_selectormap(string)Node selector for the centralized monitoring workloads.

Not applied to the Alloy agent: it is a DaemonSet that must reach every node to collect logs and node metrics, so constraining it to a workload pool would silently stop collection everywhere else.

map[]
object_storageschemaBuckets and workload identity for the logging and metrics backends, supplied by the per-cloud wrapper module. Leave null to configure storage yourself through `additional_values`.

cloud selects the objstore dialect. The *_service_account_annotations maps carry the workload-identity annotation for each component’s ServiceAccount — the chart validates that the annotation’s cloud matches the objstore backend, so a mismatched pair fails at render time rather than at pod start.

azure_storage_account is required when cloud is azure, and only then: both Loki and Thanos name the account separately from the container. Azure needs nothing else — the annotation plus the pod label the module applies are all the Entra webhook requires.

endpoint applies to aws only, and both backends need one — their shared objstore client rejects an empty endpoint rather than resolving the AWS SDK’s regional default. It is optional because the module derives s3.<region>.amazonaws.com from region, falling back to the global host when neither is given; name it to point at a VPC endpoint or an S3-compatible store.

object({
    cloud                               = string
    loki_bucket                         = string
    thanos_bucket                       = string
    region                              = optional(string)
    endpoint                            = optional(string)
    azure_storage_account               = optional(string)
    loki_service_account_annotations    = optional(map(string), {})
    thanos_service_account_annotations  = optional(map(string), {})
    gateway_service_account_annotations = optional(map(string), {})
  })
object_storage_access_key_idstringStatic access key for an S3-compatible object store, for deployments that have no workload identity to bind to — an on-prem or self-hosted store (MinIO, rustfs, Ceph), or a cluster whose IAM provider does not trust its OIDC issuer.

Prefer workload identity wherever it exists: it rotates, and the object_storage.*_service_account_annotations maps are how it is configured. These two variables are the fallback, not the default path.

Set both or neither. aws only — GCS takes a service-account key and Azure a storage-account key, neither of which is an access-key pair; configure those through additional_values.

&{}
object_storage_secret_access_keystringSecret key paired with `object_storage_access_key_id`.

Reaches the backends as a Secret in both cases, never a ConfigMap: Thanos already renders its objstore config into one, and the module switches Loki’s configStorageType to Secret when these are set — the chart’s default puts the rendered config in a ConfigMap, which would publish this value to anyone with read access to the namespace.

&{}
otlp_auth_bearer_tokenstringBearer token for `otlp_metrics`, for endpoints that take an `Authorization: Bearer` header rather than a vendor-specific one.

Delivered as a Secret this module creates (mzmon-alloy-gateway-env) rather than through the Helm values. Cannot be combined with otlp_auth_header_secrets or otlp_metrics.auth_headers: the chart has one auth slot per OTLP destination.

&{}
otlp_auth_header_secretsmap(string)Secret request headers for `otlp_metrics`, as header name to value — Honeycomb's `x-honeycomb-team`, for instance. This is the API-key-header case, which is how most OTLP vendors authenticate.

Each value is delivered as a Secret this module creates (mzmon-alloy-gateway-env) rather than through the Helm values, and the gateway reads it from the environment at startup. The module derives the variable name from the header (x-honeycomb-team becomes GATEWAY_OTEL_DEST_HEADER_X_HONEYCOMB_TEAM); nothing else depends on it.

Non-secret headers belong in otlp_metrics.auth_headers, which renders them inline. The two compose into one header set. Cannot be combined with otlp_auth_bearer_token: the chart has one auth slot per OTLP destination.

map[]
otlp_metricsschemaAlso export metrics to a generic OTLP endpoint from the Alloy gateway — Honeycomb, Grafana Cloud, or your own OpenTelemetry Collector. Null disables it; Thanos is unaffected either way.

url is a host[:port] with no scheme; https:// in it fails at gateway start. protocol selects the exporter: grpc for OTLP/gRPC, http for OTLP/HTTP.

min_importance picks a metric tier — essential, recommended, extended, diagnostic, or all — and each tier includes the ones below it. Metered backends are the reason it defaults below all here.

auth_headers sets non-secret request headers, such as Honeycomb’s x-honeycomb-dataset. They render into the gateway’s pipeline ConfigMap as literals, so put credentials in otlp_auth_header_secrets or otlp_auth_bearer_token instead — those reach the gateway through a Secret.

object({
    url            = string
    protocol       = optional(string, "grpc")
    compression    = optional(string)
    min_importance = optional(string, "recommended")
    auth_headers   = optional(map(string), {})
  })
prometheus_remote_writeschemaPrometheus remote-write destinations for the Alloy gateway, keyed by name — Amazon Managed Prometheus, Grafana Cloud, Mimir, another Thanos. Empty leaves the chart's single bundled Thanos destination exactly as it is.

The key names the destination and becomes its Alloy component label, so it must match [a-zA-Z_][a-zA-Z0-9_]* and appears in the gateway’s own metrics. Two keys are special only by convention: thanos is the chart’s built-in destination, so setting it here retunes that one rather than adding a second — which is how you drop the bundled backend to a cheaper tier, or turn it off with enabled = false while keeping another destination.

Each destination gets its own remote-write component and its own upstream tier filter, so min_importanceessential, recommended, extended, diagnostic, or all, each including the ones below it — is genuinely per destination: a metered backend on essential never buffers the metrics it would only discard. Amazon Managed Prometheus bills per sample ingested and per active series, which is what that is for.

auth_type is none, sigv4, basicAuth, or bearer. sigv4 needs no credentials at all — the gateway signs with the IRSA identity from gateway_service_account_annotations. The other two take their credentials from prometheus_remote_write_credentials, which delivers them through a Secret rather than the Helm values.

map(object({
    url             = optional(string)
    enabled         = optional(bool, true)
    min_importance  = optional(string, "all")
    auth_type       = optional(string, "none")
    sigv4_region    = optional(string)
    sigv4_role_arn  = optional(string)
    external_labels = optional(map(string), {})
  }))
prometheus_remote_write_credentialsschemaCredentials for `prometheus_remote_write` destinations whose `auth_type` is `basicAuth` or `bearer`, keyed by the same destination name.

Delivered as a Secret this module creates (mzmon-alloy-gateway-env) rather than through the Helm values, which are readable with helm get values and land in Terraform state. The module derives the variable names from the destination name — amp becomes GATEWAY_PROMETHEUS_DEST_AMP_USERNAME and friends — and writes those same names into the chart’s values, so the two cannot disagree.

sigv4 destinations need no entry here: they sign with the gateway pod’s IRSA identity.

map(object({
    username     = optional(string)
    password     = optional(string)
    bearer_token = optional(string)
  }))
sizingstringDeployment size. The chart's defaults target `medium`, and the small/large profiles are deltas from it, so `medium` intentionally applies no profile at all.

Profiles are read from the chart directory in this repository at the same commit as the pinned chart version, so they cannot drift from it. A profile that does not exist yet is skipped, which is how Thanos sizing will start applying once those profiles land.

medium
sql_scraper_passwordstringPassword for the SQL scraper's database user.
storage_classstringStorageClass for the PVC-backed workloads. Null uses the cluster default.

Four are PVC-backed by default: Alertmanager, the Loki ruler, and the Thanos Store Gateway and Compactor. Loki’s ingesters and Thanos Receive use node-local emptyDir by design — durability is the replication factor there, and a volume would pin them to one availability zone. The class is still fanned out to Receive so that re-enabling its persistence picks the class up rather than silently missing it.

Required where the default class cannot serve the nodes: GCP’s C4 and N4 families take only Hyperdisk, and every Persistent Disk class fails to attach with pd-balanced disk type cannot be used by <machine-type>.

Changing it on an existing install does not move the volumes. volumeClaimTemplates are immutable, so the old PVCs must be deleted first — discarding their contents.

&{}
tolerationsschemaTolerations for the monitoring workloads, including the Alloy agent DaemonSet — tolerations widen where a pod may run, which is what a DaemonSet wants.
list(object({
    key      = optional(string)
    operator = optional(string, "Equal")
    value    = optional(string)
    effect   = optional(string)
  }))

Outputs#

NameDescription
chart_versionChart version this release is pinned to.
grafana_admin_passwordGrafana admin password.
grafana_admin_secret_nameName of the Secret holding the Grafana admin credentials.
grafana_admin_userGrafana admin username.
grafana_urlIn-cluster URL for Grafana, which is what the module deploys by default. The chart can expose Grafana through grafana.ingress or grafana.service supplied via additional_values; this output does not follow that yet, so use the external hostname you configured there instead.
logs_urlLoki read endpoint (query frontend). Reads carry a tenant header; see the chart’s datasource configuration.
metrics_urlThanos Query endpoint. Prometheus-API-compatible, so consumers of a Prometheus URL keep working against it.
namespaceNamespace the monitoring stack is installed into.
release_nameName of the materialize-monitoring Helm release.
remote_write_urlThanos Receive remote-write endpoint, for writers outside this stack.
service_account_namesServiceAccount names the chart renders for storage-bound components.
workload_identity_subjectssystem:serviceaccount:<namespace>:<sa> subjects for the components that bind to cloud object storage. Use these when building IRSA / Workload Identity trust policies.