# Terraform variables




<!--
This file is automatically generated from
`terraform/modules/materialize-monitoring/variables.tf` and
`.terraform-docs.docsite.yml` by [terraform-docs](https://terraform-docs.io/).
Do not edit it by hand.

To update: edit the module's variables, then run
`make docs/content/reference/terraform/materialize-monitoring-variables.md`.
-->

# 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`](https://github.com/MaterializeInc/materialize-terraform-self-managed)
forwards the ones that matter and supplies the storage and identity inputs itself.
See [Installing via Terraform](../../../getting-started/terraform/) for the
variables you actually set in a `terraform.tfvars`.



## Inputs

<table class="tf-vars">
  <thead>
    <tr>
      <th>Variable</th>
      <th>Type</th>
      <th>Description</th>
      <th>Default / Schema</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="tf-var-name"><a name="additional_values" href="#additional_values">additional_<wbr>values</a></td>
        <td class="tf-var-type"><code>list(string)</code></td>
      <td class="tf-var-desc">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:

```hcl
additional_values = [
  <<-YAML
    grafana:
      ingress:
        enabled: true
        hosts: ["grafana.example.com"]
  YAML
]
```
</td>
        <td class="tf-var-default"><code>[]</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="certificate_duration" href="#certificate_duration">certificate_<wbr>duration</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Lifetime 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="certificate_renew_before" href="#certificate_renew_before">certificate_<wbr>renew_<wbr>before</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">How 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`.</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="certificates_enabled" href="#certificates_enabled">certificates_<wbr>enabled</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Render 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.
</td>
        <td class="tf-var-default"><code>false</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="chart_registry" href="#chart_registry">chart_<wbr>registry</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">OCI registry holding the materialize-monitoring charts. Override for a mirrored or air-gapped registry.</td>
        <td class="tf-var-default"><code>oci://ghcr.io/materializeinc/helm-charts</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="chart_version" href="#chart_version">chart_<wbr>version</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Version 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="crds_chart_version" href="#crds_chart_version">crds_<wbr>chart_<wbr>version</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Version 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.</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="create_namespace" href="#create_namespace">create_<wbr>namespace</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Whether this module creates the namespace. Defaults to false because the Materialize operator module already creates `monitoring` in the supported topology.</td>
        <td class="tf-var-default"><code>false</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="datadog_api_key" href="#datadog_api_key">datadog_<wbr>api_<wbr>key</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Datadog 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="datadog_metrics" href="#datadog_metrics">datadog_<wbr>metrics</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Also 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.
</td>
        <td class="tf-var-schema"><pre><code>object({
    site            = optional(string, "datadoghq.com")
    min_importance  = optional(string, "essential")
    metric_endpoint = optional(string)
    logs_endpoint   = optional(string)
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="enable_monitoring_crds" href="#enable_monitoring_crds">enable_<wbr>monitoring_<wbr>crds</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Install 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.
</td>
        <td class="tf-var-default"><code>true</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="enable_sql_scraper" href="#enable_sql_scraper">enable_<wbr>sql_<wbr>scraper</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Enable the SQL-on-scrape collector against environmentd.

On by default. The chart currently works with an empty password.
</td>
        <td class="tf-var-default"><code>true</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="gateway_service_account_annotations" href="#gateway_service_account_annotations">gateway_<wbr>service_<wbr>account_<wbr>annotations</a></td>
        <td class="tf-var-type"><code>map(string)</code></td>
      <td class="tf-var-desc">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.
</td>
        <td class="tf-var-default"><code>map[]</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="google_cloud_metrics" href="#google_cloud_metrics">google_<wbr>cloud_<wbr>metrics</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Also 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.
</td>
        <td class="tf-var-schema"><pre><code>object({
    min_importance = optional(string, "recommended")
    prefix         = optional(string)
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_admin_password" href="#grafana_admin_password">grafana_<wbr>admin_<wbr>password</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Grafana 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.</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_admin_user" href="#grafana_admin_user">grafana_<wbr>admin_<wbr>user</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Grafana admin username.</td>
        <td class="tf-var-default"><code>admin</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_enabled" href="#grafana_database_enabled">grafana_<wbr>database_<wbr>enabled</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Whether 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_host" href="#grafana_database_host">grafana_<wbr>database_<wbr>host</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Hostname 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`.</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_manage_password_secret" href="#grafana_database_manage_password_secret">grafana_<wbr>database_<wbr>manage_<wbr>password_<wbr>secret</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Whether 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_name" href="#grafana_database_name">grafana_<wbr>database_<wbr>name</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Name of the database Grafana owns.</td>
        <td class="tf-var-default"><code>grafana</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_password" href="#grafana_database_password">grafana_<wbr>database_<wbr>password</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Password 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_port" href="#grafana_database_port">grafana_<wbr>database_<wbr>port</a></td>
        <td class="tf-var-type"><code>number</code></td>
      <td class="tf-var-desc">Port for `grafana_database_host`.</td>
        <td class="tf-var-default"><code>5432</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_ssl_mode" href="#grafana_database_ssl_mode">grafana_<wbr>database_<wbr>ssl_<wbr>mode</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">libpq 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.
</td>
        <td class="tf-var-default"><code>require</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_database_user" href="#grafana_database_user">grafana_<wbr>database_<wbr>user</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Database user Grafana connects as. Must own `grafana_database_name`, because Grafana runs schema migrations at startup.</td>
        <td class="tf-var-default"><code>grafana</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="grafana_external_dns_names" href="#grafana_external_dns_names">grafana_<wbr>external_<wbr>dns_<wbr>names</a></td>
        <td class="tf-var-type"><code>list(string)</code></td>
      <td class="tf-var-desc">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.
</td>
        <td class="tf-var-default"><code>[]</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="install_metrics_server" href="#install_metrics_server">install_<wbr>metrics_<wbr>server</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Install 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.
</td>
        <td class="tf-var-default"><code>false</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="install_node_exporter" href="#install_node_exporter">install_<wbr>node_<wbr>exporter</a></td>
        <td class="tf-var-type"><code>bool</code></td>
      <td class="tf-var-desc">Install 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.
</td>
        <td class="tf-var-default"><code>true</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="install_timeout" href="#install_timeout">install_<wbr>timeout</a></td>
        <td class="tf-var-type"><code>number</code></td>
      <td class="tf-var-desc">Timeout 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.</td>
        <td class="tf-var-default"><code>900</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="internal_issuer_ref" href="#internal_issuer_ref">internal_<wbr>issuer_<wbr>ref</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Optional override for the issuer signing cluster-internal certificates
(those carrying `*.svc.<cluster domain>` 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.
</td>
        <td class="tf-var-schema"><pre><code>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")
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="internal_tls" href="#internal_tls">internal_<wbr>tls</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">How 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.

| Value | Profiles | State |
|---|---|---|
| `off` | none | plaintext everywhere |
| `encrypt` | `mtls` | servers serve TLS, clients verify them; no client certificates anywhere |
| `present` | `+ mtls-phase2` | clients 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-phase3` | servers 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.
</td>
        <td class="tf-var-default"><code>off</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="issuer_ref" href="#issuer_ref">issuer_<wbr>ref</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Default 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".
</td>
        <td class="tf-var-schema"><pre><code>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")
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="materialize_instance_namespace" href="#materialize_instance_namespace">materialize_<wbr>instance_<wbr>namespace</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Namespace the Materialize instance runs in. Used to scope scrape targets.</td>
        <td class="tf-var-default"><code>materialize-environment</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="materialize_operator_namespace" href="#materialize_operator_namespace">materialize_<wbr>operator_<wbr>namespace</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Namespace the Materialize operator runs in.</td>
        <td class="tf-var-default"><code>materialize</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="min_zones" href="#min_zones">min_<wbr>zones</a></td>
        <td class="tf-var-type"><code>number</code></td>
      <td class="tf-var-desc">Number 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="namespace" href="#namespace">namespace</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Namespace to install the monitoring stack into.</td>
        <td class="tf-var-default"><code>monitoring</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="node_selector" href="#node_selector">node_<wbr>selector</a></td>
        <td class="tf-var-type"><code>map(string)</code></td>
      <td class="tf-var-desc">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.
</td>
        <td class="tf-var-default"><code>map[]</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="object_storage" href="#object_storage">object_<wbr>storage</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Buckets 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.
</td>
        <td class="tf-var-schema"><pre><code>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), {})
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="object_storage_access_key_id" href="#object_storage_access_key_id">object_<wbr>storage_<wbr>access_<wbr>key_<wbr>id</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Static 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`.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="object_storage_secret_access_key" href="#object_storage_secret_access_key">object_<wbr>storage_<wbr>secret_<wbr>access_<wbr>key</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Secret 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="otlp_auth_bearer_token" href="#otlp_auth_bearer_token">otlp_<wbr>auth_<wbr>bearer_<wbr>token</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Bearer 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="otlp_auth_header_secrets" href="#otlp_auth_header_secrets">otlp_<wbr>auth_<wbr>header_<wbr>secrets</a></td>
        <td class="tf-var-type"><code>map(string)</code></td>
      <td class="tf-var-desc">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.
</td>
        <td class="tf-var-default"><code>map[]</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="otlp_metrics" href="#otlp_metrics">otlp_<wbr>metrics</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Also 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.
</td>
        <td class="tf-var-schema"><pre><code>object({
    url            = string
    protocol       = optional(string, "grpc")
    compression    = optional(string)
    min_importance = optional(string, "recommended")
    auth_headers   = optional(map(string), {})
  })</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="prometheus_remote_write" href="#prometheus_remote_write">prometheus_<wbr>remote_<wbr>write</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Prometheus 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.
</td>
        <td class="tf-var-schema"><pre><code>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), {})
  }))</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="prometheus_remote_write_credentials" href="#prometheus_remote_write_credentials">prometheus_<wbr>remote_<wbr>write_<wbr>credentials</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Credentials 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.
</td>
        <td class="tf-var-schema"><pre><code>map(object({
    username     = optional(string)
    password     = optional(string)
    bearer_token = optional(string)
  }))</code></pre></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="sizing" href="#sizing">sizing</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Deployment 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.
</td>
        <td class="tf-var-default"><code>medium</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="sql_scraper_password" href="#sql_scraper_password">sql_<wbr>scraper_<wbr>password</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">Password for the SQL scraper's database user.</td>
        <td class="tf-var-default"><code></code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="storage_class" href="#storage_class">storage_<wbr>class</a></td>
        <td class="tf-var-type"><code>string</code></td>
      <td class="tf-var-desc">StorageClass 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.
</td>
        <td class="tf-var-default"><code>&{}</code></td>
    </tr>
    <tr>
      <td class="tf-var-name"><a name="tolerations" href="#tolerations">tolerations</a></td>
        <td class="tf-var-type"><em>schema</em></td>
      <td class="tf-var-desc">Tolerations for the monitoring workloads, including the Alloy agent DaemonSet — tolerations widen where a pod may run, which is what a DaemonSet wants.</td>
        <td class="tf-var-schema"><pre><code>list(object({
    key      = optional(string)
    operator = optional(string, "Equal")
    value    = optional(string)
    effect   = optional(string)
  }))</code></pre></td>
    </tr>
  </tbody>
</table>

## Outputs

| Name | Description |
| ---- | ----------- |
| <a name="output_chart_version"></a> [chart_version](#output_chart_version) | Chart version this release is pinned to. |
| <a name="output_grafana_admin_password"></a> [grafana_admin_password](#output_grafana_admin_password) | Grafana admin password. |
| <a name="output_grafana_admin_secret_name"></a> [grafana_admin_secret_name](#output_grafana_admin_secret_name) | Name of the Secret holding the Grafana admin credentials. |
| <a name="output_grafana_admin_user"></a> [grafana_admin_user](#output_grafana_admin_user) | Grafana admin username. |
| <a name="output_grafana_url"></a> [grafana_url](#output_grafana_url) | In-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. |
| <a name="output_logs_url"></a> [logs_url](#output_logs_url) | Loki read endpoint (query frontend). Reads carry a tenant header; see the chart's datasource configuration. |
| <a name="output_metrics_url"></a> [metrics_url](#output_metrics_url) | Thanos Query endpoint. Prometheus-API-compatible, so consumers of a Prometheus URL keep working against it. |
| <a name="output_namespace"></a> [namespace](#output_namespace) | Namespace the monitoring stack is installed into. |
| <a name="output_release_name"></a> [release_name](#output_release_name) | Name of the materialize-monitoring Helm release. |
| <a name="output_remote_write_url"></a> [remote_write_url](#output_remote_write_url) | Thanos Receive remote-write endpoint, for writers outside this stack. |
| <a name="output_service_account_names"></a> [service_account_names](#output_service_account_names) | ServiceAccount names the chart renders for storage-bound components. |
| <a name="output_workload_identity_subjects"></a> [workload_identity_subjects](#output_workload_identity_subjects) | `system:serviceaccount:<namespace>:<sa>` subjects for the components that bind to cloud object storage. Use these when building IRSA / Workload Identity trust policies. |

