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
]
[]
alert_rulesschemaWhich bundled alerting rules install, and how they are tuned. Maps onto the chart's `rules.*`; see Configuring Alerting in the docs for what each does.
  • enabled — install the bundled rules at all. The chart’s default is true.
  • capabilities — what the deployment contains beyond what the chart derives, such as crdb-dedicated or cilium. A rule installs only where every capability it requires is present.
  • selected — alert names, rule-group names, or "*", to install beyond the default set.
  • disabled — alert names never to install.
  • overrides — per alert, a for_duration (the rule’s for) and labels to add or replace. An override never changes an expression. The usual reason for one is a cluster whose normal hydration takes longer than cluster-hydration-stuck’s hour.
  • environment_namespaces — where Materialize environments run. Null uses materialize_instance_namespace.
  • excluded_namespaces — namespaces no rule alerts on.
  • infra_workloads — the workloads each infrastructure tier holds (core, important, nonessential, daemonset). A tier given here replaces the chart’s list for that tier.

An unknown capability, alert or group name fails the Helm render, as does an override whose for_duration is not a duration.

object({
    enabled                = optional(bool)
    capabilities           = optional(list(string))
    selected               = optional(list(string))
    disabled               = optional(list(string))
    overrides              = optional(map(object({ for_duration = optional(string), labels = optional(map(string)) })))
    environment_namespaces = optional(list(string))
    excluded_namespaces    = optional(list(string))
    infra_workloads = optional(object({
      core         = optional(list(string))
      important    = optional(list(string))
      nonessential = optional(list(string))
      daemonset    = optional(list(string))
    }))
  })
alertingschemaWhere the bundled Alertmanager sends alerts. Maps onto the chart's `alerting.*`; see Alert Channels in the docs for worked examples.
  • preset — which severity-to-class mapping routes alerts: critical-infrastructure, important (the chart’s default), evaluation, or a key of presets.
  • presets — severity-to-class mappings to add, or cells to change, keyed by preset name.
  • unknown_severity — the severity an alert with none, or an unknown one, is routed as.
  • receivers — notification receivers keyed by name, each with class (the class or list of classes it serves), config (an Alertmanager receiver, verbatim, without name) and optional route options. Empty means no alert reaches anybody.
  • routes — root for the top-level route’s grouping and timing, and extra for routes ahead of the preset’s severity routes, in Alertmanager’s own format. Every bundled alert carries an audience label, platform, workload or development, which is the usual thing an extra route matches.
  • inhibit_rules, time_intervals, global — Alertmanager’s own blocks, verbatim.
  • templates — notification templates keyed by file name, each ending in .tmpl.
  • default_templates — make the chart’s notification templates every receiver’s default (the chart’s default is on): a readable subject and Slack body, and links into Grafana.
  • grafana_url — the address people open Grafana at, for those links. Null derives it from Grafana’s root_url, which is unset unless additional_values exposes Grafana.

The Alertmanager-native parts are typed any and passed through as written, so every integration Alertmanager documents works without a module release.

Credentials never belong here. Reference them with a receiver field’s _file variant — api_url_file, routing_key_file, credentials_file — at /etc/alertmanager/secrets/alertmanager-receivers/<key>, and supply <key> through alerting_receiver_secrets. The chart fails the render on an inline credential.

object({
    preset            = optional(string)
    presets           = optional(map(map(string)))
    unknown_severity  = optional(string)
    receivers         = optional(any)
    routes            = optional(object({ root = optional(any), extra = optional(any) }))
    inhibit_rules     = optional(any)
    time_intervals    = optional(any)
    templates         = optional(map(string))
    default_templates = optional(bool)
    grafana_url       = optional(string)
    global            = optional(any)
  })
alerting_receiver_secretsmap(string)Receiver credentials, keyed by file name: a Slack webhook URL, a PagerDuty routing key, an Opsgenie API key. Each becomes a key of the `alertmanager-receivers` Secret, which the chart mounts at `/etc/alertmanager/secrets/alertmanager-receivers/`, so a receiver reads one as `/etc/alertmanager/secrets/alertmanager-receivers/`.

Delivered as a Secret this module creates, never through the Helm values: anything in values is readable with helm get values by anyone who can read the release Secret. Alertmanager re-reads a credential file on every send, so rotating one needs no restart.

Empty creates no Secret, which is right when External Secrets Operator, Vault or a CSI driver owns alertmanager-receivers instead. When it is set, every path under that mount the receivers reference has to name one of these keys, or the plan fails.

map[]
alertmanager_namespacestringNamespace the Alertmanager pods run in, which is where `alerting_receiver_secrets` is created. Null uses `namespace`.

Set it to alertmanager under the chart’s split-namespace profile, which moves Alertmanager there. Not inferred: the module cannot see where an additional_values override put it. The namespace has to exist before apply.

&{}
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.

&{}
cluster_namestringName of the Kubernetes cluster, stamped as the `cluster` label on every log line, metric sample and alert the stack produces. Written to the chart's `clusterName`.

Set it whenever more than one cluster writes to the same log or metrics store, or notifies the same channel or incident tool — the label is the only thing that tells them apart. Null leaves the chart’s default, default.

&{}
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
dashboards_allow_cross_namespace_importboolAllow the dashboards to match a Grafana instance outside their own namespace.

Required when the Grafana resource lives somewhere other than namespace, which is what the chart’s split-namespace profile does. Not inferred: the module cannot see where an additional_values override put the instance.

Note that the CRDs forbid turning this back off in place; the resource has to be recreated.

false
dashboards_chart_versionstringVersion of the three dashboards charts, which share one version stream. Read from each chart's `Chart.yaml` when null, like `chart_version`. Tracked separately because the dashboards ship on a version stream of their own.&{}
dashboards_instance_selectormap(string)Labels selecting the Grafana instance the dashboards are pushed into.

Null derives the label the main chart puts on its own Grafana resource, which is correct unless that release narrowed the selector through connections.grafana.labels in additional_values — the dashboards charts cannot read that, so set the same map here.

&{}
dashboards_selectedlist(string)Dashboard filename-stem patterns to install, matched against every dashboard in the three dashboards charts.

Null leaves the charts’ own default, which is every dashboard. Narrow it to hold one back — env-upgrade depends on operator instrumentation that not every Materialize version has. A dashboards chart the patterns match nothing in is not installed.

&{}
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_dashboardsboolInstall the dashboards charts: materialize-monitoring-dashboards for the Materialize environment, materialize-monitoring-dashboards-infra for the platform underneath it, and materialize-monitoring-dashboards-meta for the monitoring stack itself.

The dashboards are Helm releases of their own, one per Grafana folder, because Helm stores a release in a Kubernetes Secret and a Secret may not exceed 1 MiB, which the rendered set had grown past. The module installs them against the folders and Grafana instance the main release creates, so they stay in step without either side having to discover the other.

Set false when dashboards are managed elsewhere — a platform team’s own Grafana provisioning, or a GitOps pipeline that owns them.

true
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.

Every CRD carries helm.sh/resource-policy: keep, so destroying this release, or turning this off later, leaves the CRDs and every custom resource in the cluster in place. Removing the CRDs is a deliberate step of its own; see the “Uninstalling” page in the docs.

true
enable_postgres_exporterboolRun the chart's postgres-exporter, which reads each PostgreSQL database it watches from the inside: connections against the ceiling, transaction-ID age, vacuum, and the size of every database on the instance.

It watches every postgres_exporter_targets entry, and Grafana’s own database whenever this module configures one with a password, as a target named grafana. Grafana’s connects to the postgres database rather than grafana_database_name, because Grafana’s own tables would add thousands of per-table series that no panel reads.

Each target’s credentials go into a Secret this module creates, postgres-exporter, never into the Helm values. The role each target connects as needs pg_monitor to see other roles' sessions; see the PostgreSQL exporter page.

false
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, `azure.workload.identity/client-id` for Azure workload identity. The last also labels the gateway pods `azure.workload.identity/use: "true"`, without which the Entra webhook injects nothing.

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 Managed Service for Prometheus from the Alloy gateway, over OTLP to the Telemetry API. 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: the series land as prometheus.googleapis.com/ metrics, billed per sample ingested, and all sends the entire surface.

The project needs the telemetry.googleapis.com API enabled. 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.

prefix is deprecated and ignored. The Telemetry API names every metric prometheus.googleapis.com/<name>/<kind>, so there is no prefix to choose.

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 ports stop short of authenticate by nature rather than by choice, and the chart refuses to configure them otherwise: Loki’s HTTP port and Alertmanager’s API port are probed by the kubelet, and a httpGet probe has no field for a client certificate, so their terminal state is present. The Loki gateway’s listener stops at present for the same reason. Grafana’s datasources verify the backend, and from present they present a certificate to Alertmanager and to the Loki gateway; the Thanos datasource presents none.

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")
  })
kube_state_metrics_pod_labelslist(string)Pod labels to publish on `kube_pod_labels`, in addition to the Materialize ones the chart names.

An ownership label for cost-center allocation is the expected use. Each arrives as label_<key>, with every character outside [a-zA-Z0-9_] mapped to _, and joins to any kube_pod_* family on namespace and pod.

Appended to the chart’s pods allowlist entry rather than replacing it. Setting kube-state-metrics.metricLabelsAllowlist through additional_values instead replaces the whole list, node labels included.

Name each label. * copies every label on every pod, and is refused.

[]
materialize_instance_namespacestringNamespace the Materialize instance runs in. Used to scope scrape targets.materialize-environment
materialize_operator_namespacestringNamespace the Materialize operator runs in.materialize
metadata_databasesschemaThe metadata databases a cloud provider pull watches, by flavor and the provider's name for them. Each becomes an `externalDependencies.consensus` entry, so the `ext:consensus_*` recording rules read that database and no other the pull also watches, such as Grafana's.

flavor is rds (the DB instance identifier), cloudsql (the instance name, without the project) or azure-postgres (the flexible server name). A postgres-exporter target is declared with its own metadata_database flag instead.

list(object({
    flavor      = string
    resource_id = string
  }))
min_zonesnumberNumber of availability zones the node pool can actually launch in, used to adjust the hard zone spread on Thanos Receive, Loki's ingesters and Alertmanager. 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. Alertmanager’s rule sets no minDomains, so it survives one zone and breaks only at zero.

&{}
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), {})
  })
postgres_exporter_passwordsmap(string)The password for each `postgres_exporter_targets` entry, keyed by its `name`. Written into the exporter's Secret and nowhere else. Every target needs a non-empty one: the exporter refuses a target whose credentials have no password. Grafana's always comes from `grafana_database_password`, and a `grafana` entry here is ignored.map[]
postgres_exporter_targetsschemaDatabases for the postgres-exporter to watch, beside Grafana's. Ignored unless `enable_postgres_exporter` is true.

name becomes the target’s target label and keys its password in postgres_exporter_passwords. database is the one the connection opens, and per-table statistics come from it alone; everything instance-wide covers every database on the instance. Point it at the metadata database’s own database (materialize in the shipped examples), and leave it at postgres for anything whose tables you do not need.

metadata_database = true declares the target a metadata database, which is what the postgres recording-rule adapter reads.

list(object({
    name              = string
    host              = string
    port              = optional(number, 5432)
    database          = optional(string, "postgres")
    user              = string
    ssl_mode          = optional(string, "require")
    metadata_database = optional(bool, false)
  }))
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_importance — essential, 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 Thanos ruler runs stateless and holds only a remote-write WAL. The class is still fanned out to Receive and to the ruler so that re-enabling their 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 (the Loki gateway), which also serves the ruler’s rule and alert state. https whenever internal_tls is not off, verified against the internal CA. 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.