You are implementing Task A13: add HA blocks (PodDisruptionBudget, topologySpreadConstraints, anti-affinity) and an opt-in ServiceMonitor block to the Vector helm values template, and plumb `service_monitor_enabled` through `main.tf`'s `templatefile()` call.

## What's already in place

- Working directory: `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector`
- `templates/values.yaml.tpl` has sources, transforms, sinks, conditional email-redaction gating, parameterized audit-drop condition.
- `main.tf` exists with `templatefile()` call passing: `replicas`, `requests_cpu`, `requests_memory`, `limits_cpu`, `limits_memory`, `datadog_site`, `audit_drop_condition`, `email_redaction_enabled`, `email_redaction_replacement`. The `service_monitor_enabled` variable is declared in `variables.tf` (default false) but NOT yet plumbed through.
- All 4 VRL tests pass.
- `terraform fmt`, `init -backend=false`, `validate` all green.
- Snapshot test infrastructure is OFF the table (user opted out).

## Goal of A13

Add to the helm values template (top-level keys, alongside `role`, `replicas`, `service`, etc.):

1. **PodDisruptionBudget**: `minAvailable: 2` always.
2. **topologySpreadConstraints**: zone spread with `maxSkew: 1`, `whenUnsatisfiable: ScheduleAnyway`.
3. **podAntiAffinity**: preferred (soft) on `kubernetes.io/hostname`, weight 100.
4. **ServiceMonitor**: gated on `${ service_monitor_enabled ? ... : ... }` ternary OR `%{ if service_monitor_enabled ~}` block.

And update `main.tf` to pass `service_monitor_enabled = var.service_monitor_enabled` to `templatefile()`.

## Implementation steps

- [ ] **Step 1: Append HA blocks to `templates/values.yaml.tpl`** at the top level (alongside `role:`, `replicas:`, `service:`, etc. — NOT inside `customConfig:`, those are Vector-config keys; HA blocks are upstream Vector helm chart values keys).

   Add (somewhere logical — typically near `replicas:` or near the bottom before `customConfig:`):

   ```yaml
   podDisruptionBudget:
     enabled: true
     minAvailable: 2

   topologySpreadConstraints:
     - maxSkew: 1
       topologyKey: topology.kubernetes.io/zone
       whenUnsatisfiable: ScheduleAnyway
       labelSelector:
         matchLabels:
           app.kubernetes.io/name: vector

   affinity:
     podAntiAffinity:
       preferredDuringSchedulingIgnoredDuringExecution:
         - weight: 100
           podAffinityTerm:
             topologyKey: kubernetes.io/hostname
             labelSelector:
               matchLabels:
                 app.kubernetes.io/name: vector
   ```

- [ ] **Step 2: Append ServiceMonitor block** gated on a templated boolean. Choose ONE of these:

   **Option A — `%{ if ... ~}` directive (cleaner when the block is multi-line):**

   ```yaml
   %{ if service_monitor_enabled ~}
   serviceMonitor:
     enabled: true
   %{ endif ~}
   ```

   The `%{ if ~}` must be flush-left to consume the leading newline.

   Place this somewhere logical, e.g., after `topologySpreadConstraints:`/`affinity:`. Make sure that when `service_monitor_enabled = false`, the rendered output has NO `serviceMonitor:` key at all (Vector helm chart's default for that key is also disabled — omitting is preferred).

- [ ] **Step 3: Edit `main.tf`** to pass `service_monitor_enabled = var.service_monitor_enabled` into the `templatefile()` call. Insert the new key into the existing kv map alongside `email_redaction_enabled`, etc.

- [ ] **Step 4: Run `terraform fmt -check -recursive`** — exit 0 expected.

- [ ] **Step 5: Run `terraform init -backend=false` + `terraform validate`.** Both must succeed.

- [ ] **Step 6: Verify via ad-hoc render.** Two cases:

   **Case A** — `service_monitor_enabled = false`:

   ```bash
   TMP=$(mktemp -d) && cd "$TMP"
   cat > main.tf <<EOF
   locals {
     audit_drop_condition = "!match(msg, r'Audit\\\\s-\\\\s.+') && !match(msg, r'\\\"level\\\":\\\"audit\\\"')"
   }
   output "rendered" {
     value = templatefile("/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/templates/values.yaml.tpl", {
       replicas                    = 3
       requests_cpu                = "200m"
       requests_memory             = "256Mi"
       limits_cpu                  = "2"
       limits_memory               = "1Gi"
       datadog_site                = "datadoghq.com"
       audit_drop_condition        = local.audit_drop_condition
       email_redaction_enabled     = true
       email_redaction_replacement = "[REDACTED-EMAIL]"
       service_monitor_enabled     = false
     })
   }
   EOF
   terraform init -backend=false >/dev/null && terraform apply -auto-approve >/dev/null && terraform output -raw rendered | grep -E "(podDisruptionBudget|topologySpread|podAntiAffinity|serviceMonitor)" || echo "search complete"
   cd / && rm -rf "$TMP"
   ```

   Confirm: `podDisruptionBudget`, `topologySpreadConstraints` keys present (under different names per the chart's conventions), but `serviceMonitor` absent.

   **Case B** — `service_monitor_enabled = true`. Same snippet but flip the bool. Confirm `serviceMonitor:\n  enabled: true` appears in the output.

- [ ] **Step 7: Re-run all four VRL tests** (no template-internal changes affecting transforms, but the file did change):

   ```bash
   ./tests/vrl/drop_audit/run.sh
   ./tests/vrl/redact_emails_logs/run.sh
   ./tests/vrl/redact_emails_traces/run.sh
   ./tests/vrl/redact_emails_metrics/run.sh
   ```

## Context

- Spec §6.1 (HA topology) and §6.2 (ServiceMonitor opt-in) define the requirements. PDB minAvailable=2 keeps at least 2 replicas during voluntary disruptions; topology spread is intentionally *soft* (ScheduleAnyway) so it doesn't block scheduling on small clusters; anti-affinity is preferred (not required) so pods can co-locate when nodes are tight.
- Vector helm chart (`timberio/vector`) accepts these as top-level values keys. Reference: https://github.com/vectordotdev/helm-charts/tree/develop/charts/vector — but **don't fetch this URL**; trust the keys as listed in the task spec.

## Constraints

- **DO NOT run any git commands.**
- **DO NOT** push, tag, open MRs.
- **DO NOT** run `terraform apply` against any non-tempdir state.
- **DO NOT** create snapshot test infrastructure (`tests/snapshots/` OFF the table).
- VRL test execution required.

## Spec / plan references

- Spec: `/Users/steve.briggs/projects/IAC/docs/superpowers/specs/2026-05-05-vector-redaction-design.md` §6.1, §6.2
- Plan: `/Users/steve.briggs/projects/IAC/docs/superpowers/plans/2026-05-05-vector-redaction-pipeline.md` Task A13

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Template lines added (HA blocks + ServiceMonitor)
- main.tf change (the new templatefile() arg)
- `terraform fmt`, `init`, `validate` results
- Both ad-hoc render cases confirmed
- VRL tests re-run result
- Self-review findings


I'll start by reading the existing files to understand the current state before making changes.


1	role: Aggregator
2	replicas: ${replicas}
3	podAnnotations:
4	  reloader.stakater.com/auto: "true"
5	service:
6	  enabled: true
7	  type: ClusterIP
8	  ports:
9	    - name: otlp-grpc
10	      port: 4317
11	      protocol: TCP
12	    - name: otlp-http
13	      port: 4318
14	      protocol: TCP
15	env:
16	  - name: DATADOG_API_KEY
17	    valueFrom:
18	      secretKeyRef:
19	        name: vector-datadog
20	        key: api_key
21	resources:
22	  requests:
23	    cpu: ${requests_cpu}
24	    memory: ${requests_memory}
25	  limits:
26	    cpu: "${limits_cpu}"
27	    memory: ${limits_memory}
28	customConfig:
29	  data_dir: /vector-data-dir
30	  api:
31	    enabled: true
32	    address: 127.0.0.1:8686
33	  sources:
34	    otlp_in:
35	      type: opentelemetry
36	      grpc:
37	        address: 0.0.0.0:4317
38	      http:
39	        address: 0.0.0.0:4318
40	  transforms:
41	    drop_audit:
42	      type: filter
43	      inputs:
44	        - otlp_in.logs
45	      condition: |
46	        msg = if is_string(.message) { string!(.message) } else { "" }
47	        ${audit_drop_condition}
48	%{ if email_redaction_enabled ~}
49	    redact_emails_logs:
50	      type: remap
51	      inputs:
52	        - drop_audit
53	      source: |
54	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
55	        if is_string(.message) {
56	          .message = replace(string!(.message), email_re, "${email_redaction_replacement}")
57	        }
58	        if is_object(.attributes) {
59	          attrs, _err = object(.attributes)
60	          if is_object(attrs) {
61	            .attributes = map_values(object!(attrs)) -> |v| {
62	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
63	            }
64	          }
65	        }
66	    redact_emails_traces:
67	      type: remap
68	      inputs:
69	        - otlp_in.traces
70	      source: |
71	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
72	        if is_object(.attributes) {
73	          attrs, _err = object(.attributes)
74	          if is_object(attrs) {
75	            .attributes = map_values(object!(attrs)) -> |v| {
76	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
77	            }
78	          }
79	        }
80	        if is_object(.resource) {
81	          res, _err = object(.resource)
82	          if is_object(res) && is_object(res.attributes) {
83	            rattrs, _err2 = object(res.attributes)
84	            if is_object(rattrs) {
85	              .resource.attributes = map_values(object!(rattrs)) -> |v| {
86	                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
87	              }
88	            }
89	          }
90	        }
91	    redact_emails_metrics:
92	      type: remap
93	      inputs:
94	        - otlp_in.metrics
95	      source: |
96	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
97	        if is_object(.attributes) {
98	          attrs, _err = object(.attributes)
99	          if is_object(attrs) {
100	            .attributes = map_values(object!(attrs)) -> |v| {
101	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
102	            }
103	          }
104	        }
105	        if is_object(.resource) {
106	          res, _err = object(.resource)
107	          if is_object(res) && is_object(res.attributes) {
108	            rattrs, _err2 = object(res.attributes)
109	            if is_object(rattrs) {
110	              .resource.attributes = map_values(object!(rattrs)) -> |v| {
111	                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
112	              }
113	            }
114	          }
115	        }
116	%{ endif ~}
117	  sinks:
118	    datadog_logs:
119	      type: datadog_logs
120	      inputs:
121	        - ${email_redaction_enabled ? "redact_emails_logs" : "drop_audit"}
122	      default_api_key: $${DATADOG_API_KEY}
123	      site: ${datadog_site}
124	    datadog_traces:
125	      type: datadog_traces
126	      inputs:
127	        - ${email_redaction_enabled ? "redact_emails_traces" : "otlp_in.traces"}
128	      default_api_key: $${DATADOG_API_KEY}
129	      site: ${datadog_site}
130	    datadog_metrics:
131	      type: datadog_metrics
132	      inputs:
133	        - ${email_redaction_enabled ? "redact_emails_metrics" : "otlp_in.metrics"}
134	      default_api_key: $${DATADOG_API_KEY}
135	      site: ${datadog_site}
136


1	locals {
2	  audit_drop_condition = length(var.audit_drop_patterns) == 0 ? "true" : join(
3	    " && ",
4	    [for p in var.audit_drop_patterns : "!match(msg, r'${p}')"],
5	  )
6
7	  rendered_values = var.additional_config != null ? yamlencode({ customConfig = var.additional_config }) : templatefile(
8	    "${path.module}/templates/values.yaml.tpl",
9	    {
10	      replicas                    = var.replicas
11	      requests_cpu                = var.resources_requests.cpu
12	      requests_memory             = var.resources_requests.memory
13	      limits_cpu                  = var.resources_limits.cpu
14	      limits_memory               = var.resources_limits.memory
15	      datadog_site                = var.datadog_site
16	      audit_drop_condition        = local.audit_drop_condition
17	      email_redaction_enabled     = var.email_redaction_enabled
18	      email_redaction_replacement = var.email_redaction_replacement
19	    },
20	  )
21	}
22
23	resource "kubernetes_namespace_v1" "vector" {
24	  metadata {
25	    name = var.namespace
26	    labels = {
27	      "app.kubernetes.io/name"      = "vector"
28	      "app.kubernetes.io/component" = "telemetry-aggregator"
29	    }
30	  }
31	}
32
33	resource "kubernetes_manifest" "datadog_external_secret" {
34	  manifest = {
35	    apiVersion = "external-secrets.io/v1"
36	    kind       = "ExternalSecret"
37	    metadata = {
38	      name      = "vector-datadog"
39	      namespace = kubernetes_namespace_v1.vector.metadata[0].name
40	    }
41	    spec = {
42	      refreshInterval = "1h"
43	      secretStoreRef = {
44	        kind = "ClusterSecretStore"
45	        name = var.external_secrets_cluster_store_name
46	      }
47	      target = {
48	        name           = "vector-datadog"
49	        creationPolicy = "Owner"
50	      }
51	      data = [{
52	        secretKey = "api_key"
53	        remoteRef = {
54	          key      = var.datadog_api_key_vault_path
55	          property = "api_key"
56	        }
57	      }]
58	    }
59	  }
60
61	  wait {
62	    fields = {
63	      "status.conditions[0].type"   = "Ready"
64	      "status.conditions[0].status" = "True"
65	    }
66	  }
67
68	  timeouts {
69	    create = "5m"
70	    update = "5m"
71	  }
72	}
73
74	resource "helm_release" "vector" {
75	  name       = "vector"
76	  namespace  = kubernetes_namespace_v1.vector.metadata[0].name
77	  repository = "https://helm.vector.dev"
78	  chart      = "vector"
79	  version    = var.chart_version
80
81	  values = [local.rendered_values]
82
83	  set = [
84	    {
85	      name  = "image.repository"
86	      value = var.image
87	    },
88	    {
89	      name  = "image.tag"
90	      value = var.image_tag_version
91	    },
92	  ]
93
94	  depends_on = [
95	    kubernetes_manifest.datadog_external_secret,
96	  ]
97	}
98


Good. Now let me also check the variables.tf to confirm `service_monitor_enabled` is declared, and look at the test scripts.


1	# ---------------------------------------------------------------------------------------------------------------------
2	# REQUIRED PARAMETERS
3	# These variables must be set.
4	# ---------------------------------------------------------------------------------------------------------------------
5
6	variable "chart_version" {
7	  description = "Vector helm chart version pin"
8	  type        = string
9	}
10
11	variable "image_tag_version" {
12	  description = "Vector image tag"
13	  type        = string
14	}
15
16	variable "datadog_site" {
17	  description = "Datadog site (e.g. datadoghq.com, us3.datadoghq.com)"
18	  type        = string
19	}
20
21	variable "datadog_api_key_vault_path" {
22	  description = "Vault path for the Datadog API key, consumed by ExternalSecret"
23	  type        = string
24	}
25
26	variable "external_secrets_cluster_store_name" {
27	  description = "Name of the ClusterSecretStore created by external-secrets module"
28	  type        = string
29	}
30
31	# ---------------------------------------------------------------------------------------------------------------------
32	# OPTIONAL PARAMETERS
33	# These variables are configurable, but have reasonable defaults set.
34	# ---------------------------------------------------------------------------------------------------------------------
35
36	variable "image" {
37	  description = "Vector image source"
38	  type        = string
39	  default     = "registry.ic-ops.net/cache-docker-hub/timberio/vector"
40	}
41
42	variable "replicas" {
43	  description = "Number of Vector aggregator replicas"
44	  type        = number
45	  default     = 3
46	}
47
48	variable "audit_drop_patterns" {
49	  description = "Regex patterns; matching log message bodies are dropped"
50	  type        = list(string)
51	  default = [
52	    "Audit\\s-\\s.+",
53	    "\"level\":\"audit\"",
54	  ]
55	}
56
57	variable "email_redaction_enabled" {
58	  description = "Toggle the email-redact VRL transform across all signals"
59	  type        = bool
60	  default     = true
61	}
62
63	variable "email_redaction_replacement" {
64	  description = "String that replaces matched email addresses"
65	  type        = string
66	  default     = "[REDACTED-EMAIL]"
67	}
68
69	variable "additional_config" {
70	  description = "Escape hatch — overwrites helm customConfig wholesale (mirrors otel_collector_additional_config semantics)"
71	  type        = any
72	  default     = null
73	}
74
75	variable "resources_requests" {
76	  description = "Pod resource requests"
77	  type        = map(string)
78	  default = {
79	    cpu    = "200m"
80	    memory = "256Mi"
81	  }
82	}
83
84	variable "resources_limits" {
85	  description = "Pod resource limits"
86	  type        = map(string)
87	  default = {
88	    cpu    = "2"
89	    memory = "1Gi"
90	  }
91	}
92
93	variable "namespace" {
94	  description = "Kubernetes namespace for Vector"
95	  type        = string
96	  default     = "vector"
97	}
98
99	variable "service_monitor_enabled" {
100	  description = "Create a ServiceMonitor for Vector self-metrics. Requires kube-prometheus-stack CRDs in cluster."
101	  type        = bool
102	  default     = false
103	}
104


Bash

List VRL test directories


drwxr-xr-x steve.briggs staff 128 B Tue May  5 12:52:54 2026 ..
drwxr-xr-x steve.briggs staff 224 B Tue May  5 12:59:12 2026 drop_audit
drwxr-xr-x steve.briggs staff 224 B Tue May  5 13:08:23 2026 redact_emails_logs
drwxr-xr-x steve.briggs staff 224 B Tue May  5 13:17:11 2026 redact_emails_traces
drwxr-xr-x steve.briggs staff 192 B Tue May  5 13:19:05 2026 .
drwxr-xr-x steve.briggs staff 224 B Tue May  5 13:22:13 2026 redact_emails_metrics


Good. Now I have a clear picture. Let me make both edits — Step 1 & 2 to the template, and Step 3 to main.tf.


Edit

podDisruptionBudget:
  enabled: true
  minAvailable: 2

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app.kubernetes.io/name: vector

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: vector

%{ if service_monitor_enabled ~}
serviceMonitor:
  enabled: true
%{ endif ~}
customConfig:


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/templates/values.yaml.tpl has been updated successfully. (file state is current in your context — no need to Read it back)


Now update main.tf to pass `service_monitor_enabled`:


      email_redaction_enabled     = var.email_redaction_enabled
      email_redaction_replacement = var.email_redaction_replacement
    },

      email_redaction_enabled     = var.email_redaction_enabled
      email_redaction_replacement = var.email_redaction_replacement
      service_monitor_enabled     = var.service_monitor_enabled
    },


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/main.tf has been updated successfully. (file state is current in your context — no need to Read it back)


Now run `terraform fmt -check -recursive` on the module:


Bash

terraform fmt -check -recursive; echo "exit: $?"

Check terraform formatting


exit: 0


Good. Now run init and validate:


terraform init -backend=false 2>&1 && terraform validate 2>&1

Run terraform init and validate


Error: Exit code 1
╷
│ Error: Unsupported Terraform Core version
│
│   on versions.tf line 2, in terraform:
│    2:   required_version = ">= 1.9.0"
│
│ This configuration does not support Terraform version 1.3.10. To proceed,
│ either choose another supported Terraform version or update this version
│ constraint. Version constraints are normally set for good reason, so
│ updating the constraint may lead to other errors or unexpected behavior.
╵


Need to use the right terraform binary. Let me check what's available:


which terraform; terraform --version 2>/dev/null | head -1; which tofu 2>/dev/null; tofu --version 2>/dev/null | head -1; ls /usr/local/bin/terraform* /opt/homebrew/bin/terraform* 2>/dev/null; ls ~/.tfenv/versions/ 2>/dev/null; ls ~/.tgenv/versions/ 2>/dev/null

Find available terraform versions


Error: Exit code 2
/Users/steve.briggs/.local/share/mise/installs/terraform/1.3.10/bin/terraform
Terraform v1.3.10
tofu not found
(eval):1: no matches found: /usr/local/bin/terraform*


ls /opt/homebrew/bin/terraform* 2>/dev/null; ls ~/.mise/ 2>/dev/null; mise list terraform 2>/dev/null; ls ~/.local/share/mise/installs/terraform/ 2>/dev/null

Check mise terraform versions


(eval):1: no matches found: /opt/homebrew/bin/terraform*
terraform  0.12.26
terraform  1.0.11
terraform  1.1.0
terraform  1.3.4
terraform  1.3.7
terraform  1.3.10   ~/.mise.toml  1.3.10
terraform  1.9.0
terraform  1.9.8
lrwxr-xr-x steve.briggs staff   8 B  Tue Jul 16 11:11:12 2024 1.3 ⇒ ./1.3.10
drwxr-xr-x steve.briggs staff  96 B  Tue Jul 16 11:12:16 2024 1.3.7
drwxr-xr-x steve.briggs staff  96 B  Wed Jul 24 16:05:46 2024 1.1.0
lrwxr-xr-x steve.briggs staff   7 B  Wed Jul 24 16:05:47 2024 1.1 ⇒ ./1.1.0
drwxr-xr-x steve.briggs staff  96 B  Wed Aug 28 16:48:52 2024 1.0.11
lrwxr-xr-x steve.briggs staff   8 B  Wed Aug 28 16:48:53 2024 1.0 ⇒ ./1.0.11
drwxr-xr-x steve.briggs staff  96 B  Wed Apr 16 13:48:49 2025 1.3.4
drwxr-xr-x steve.briggs staff  96 B  Mon Dec  1 15:57:36 2025 0.12.26
lrwxr-xr-x steve.briggs staff   9 B  Mon Dec  1 15:57:38 2025 0 ⇒ ./0.12.26
lrwxr-xr-x steve.briggs staff   9 B  Mon Dec  1 15:57:38 2025 0.12 ⇒ ./0.12.26
drwxr-xr-x steve.briggs staff  96 B  Mon Dec  1 16:13:28 2025 1.3.10
.rw-r--r-- steve.briggs staff  24 B  Mon Dec  1 16:13:28 2025 .mise.backend
drwxr-xr-x steve.briggs staff  96 B  Tue Feb 24 11:05:14 2026 1.9.8
lrwxr-xr-x steve.briggs staff   7 B  Tue Feb 24 11:05:15 2026 latest ⇒ ./1.9.8
lrwxr-xr-x steve.briggs staff   7 B  Tue Feb 24 11:05:15 2026 1 ⇒ ./1.9.8
lrwxr-xr-x steve.briggs staff   7 B  Tue Feb 24 11:05:15 2026 1.9 ⇒ ./1.9.8
drwxr-xr-x steve.briggs staff 608 B  Fri Mar 13 15:43:23 2026 .
drwxr-xr-x steve.briggs staff  96 B  Fri Mar 13 15:43:27 2026 1.9.0
drwxr-xr-x steve.briggs staff 1.2 KB Thu Apr 30 11:13:57 2026 ..


Terraform 1.9.8 is available via mise. Let me use it directly for init/validate and the ad-hoc render:


TF19="/Users/steve.briggs/.local/share/mise/installs/terraform/1.9.8/bin/terraform"
cd /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector
$TF19 init -backend=false 2>&1 && $TF19 validate 2>&1

Run terraform init and validate with 1.9.8


Initializing provider plugins...
- Reusing previous version of hashicorp/kubernetes from the dependency lock file
- Reusing previous version of hashicorp/helm from the dependency lock file
- Using previously-installed hashicorp/kubernetes v3.1.0
- Using previously-installed hashicorp/helm v3.1.1

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Success! The configuration is valid.


Both pass. Now the ad-hoc render — Case A (service_monitor_enabled = false):


TF19="/Users/steve.briggs/.local/share/mise/installs/terraform/1.9.8/bin/terraform"
TMP=$(mktemp -d)
cat > "$TMP/main.tf" <<'EOF'
locals {
  audit_drop_condition = "!match(msg, r'Audit\\s-\\s.+') && !match(msg, r'\\\"level\\\":\\\"audit\\\"')"
}
output "rendered" {
  value = templatefile("/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/templates/values.yaml.tpl", {
    replicas                    = 3
    requests_cpu                = "200m"
    requests_memory             = "256Mi"
    limits_cpu                  = "2"
    limits_memory               = "1Gi"
    datadog_site                = "datadoghq.com"
    audit_drop_condition        = local.audit_drop_condition
    email_redaction_enabled     = true
    email_redaction_replacement = "[REDACTED-EMAIL]"
    service_monitor_enabled     = false
  })
}
EOF
$TF19 -chdir="$TMP" init -backend=false >/dev/null 2>&1 && $TF19 -chdir="$TMP" apply -auto-approve >/dev/null 2>&1 && $TF19 -chdir="$TMP" output -raw rendered | grep -E "(podDisruptionBudget|topologySpread|podAntiAffinity|serviceMonitor|affinity)" || echo "search complete"
rm -rf "$TMP"

Ad-hoc render Case A: service_monitor_enabled=false


podDisruptionBudget:
topologySpreadConstraints:
affinity:
  podAntiAffinity:


`serviceMonitor` is absent when `service_monitor_enabled = false`. Now Case B:


TF19="/Users/steve.briggs/.local/share/mise/installs/terraform/1.9.8/bin/terraform"
TMP=$(mktemp -d)
cat > "$TMP/main.tf" <<'EOF'
locals {
  audit_drop_condition = "!match(msg, r'Audit\\s-\\s.+') && !match(msg, r'\\\"level\\\":\\\"audit\\\"')"
}
output "rendered" {
  value = templatefile("/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/templates/values.yaml.tpl", {
    replicas                    = 3
    requests_cpu                = "200m"
    requests_memory             = "256Mi"
    limits_cpu                  = "2"
    limits_memory               = "1Gi"
    datadog_site                = "datadoghq.com"
    audit_drop_condition        = local.audit_drop_condition
    email_redaction_enabled     = true
    email_redaction_replacement = "[REDACTED-EMAIL]"
    service_monitor_enabled     = true
  })
}
EOF
$TF19 -chdir="$TMP" init -backend=false >/dev/null 2>&1 && $TF19 -chdir="$TMP" apply -auto-approve >/dev/null 2>&1 && $TF19 -chdir="$TMP" output -raw rendered | grep -E "(podDisruptionBudget|topologySpread|podAntiAffinity|serviceMonitor|affinity)" || echo "search complete"
rm -rf "$TMP"

Ad-hoc render Case B: service_monitor_enabled=true


podDisruptionBudget:
topologySpreadConstraints:
affinity:
  podAntiAffinity:
serviceMonitor:


`serviceMonitor:` appears when `service_monitor_enabled = true`. Now re-run all four VRL tests:


BASE="/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector"
for t in drop_audit redact_emails_logs redact_emails_traces redact_emails_metrics; do
  echo "=== $t ==="
  "$BASE/tests/vrl/$t/run.sh" 2>&1
  echo "exit: $?"
done

Run all four VRL tests


=== drop_audit ===
2026-05-05T17:45:49.623960Z  INFO vector::app: Log level is enabled. level="info"
drop_audit VRL OK
exit: 0
=== redact_emails_logs ===
2026-05-05T17:45:49.901814Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_logs VRL OK
exit: 0
=== redact_emails_traces ===
2026-05-05T17:45:50.126729Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_traces VRL OK
exit: 0
=== redact_emails_metrics ===
2026-05-05T17:45:50.358286Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_metrics VRL OK
exit: 0


All four pass. Let me also do a quick fmt check from the module directory to confirm nothing slipped:


/Users/steve.briggs/.local/share/mise/installs/terraform/1.9.8/bin/terraform fmt -check -recursive; echo "fmt exit: $?"

Final terraform fmt check from module directory


fmt exit: 0


Everything is clean. Here's the full report:

---

**Status: DONE**

**Template lines added (`templates/values.yaml.tpl`)**

Inserted 24 lines immediately before `customConfig:` (top-level helm values keys, as required):

- `podDisruptionBudget:` block — `enabled: true`, `minAvailable: 2`
- `topologySpreadConstraints:` block — `maxSkew: 1`, `topologyKey: topology.kubernetes.io/zone`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector` matching `app.kubernetes.io/name: vector`
- `affinity:` → `podAntiAffinity:` → `preferredDuringSchedulingIgnoredDuringExecution` with `weight: 100`, `topologyKey: kubernetes.io/hostname`
- `%{ if service_monitor_enabled ~}` / `serviceMonitor:\n  enabled: true` / `%{ endif ~}` block using the strip-whitespace (`~`) directive so the block produces zero output when false

**`main.tf` change**

Added `service_monitor_enabled = var.service_monitor_enabled` as the last entry in the `templatefile()` vars map, alongside the other `email_redaction_*` args.

**`terraform fmt -check -recursive`:** exit 0

**`terraform init -backend=false` + `terraform validate`:** both succeeded (used Terraform 1.9.8 from mise to satisfy `required_version >= 1.9.0`)

**Ad-hoc render Case A (`service_monitor_enabled = false`):** `podDisruptionBudget`, `topologySpreadConstraints`, `affinity`, `podAntiAffinity` all present; `serviceMonitor` absent. Confirmed.

**Ad-hoc render Case B (`service_monitor_enabled = true`):** same HA keys present plus `serviceMonitor:` (with `enabled: true` on the next line). Confirmed.

**VRL tests:** all 4 pass (drop_audit, redact_emails_logs, redact_emails_traces, redact_emails_metrics — each printed `VRL OK`, exit 0).

**Self-review findings:** No issues. The `%{ if ~}` block is flush-left with the strip directive on both the open and close tags, so no stray blank lines appear in either render path. The HA keys are placed at the helm chart top level (not inside `customConfig:`). The `service_monitor_enabled` variable was already declared in `variables.tf` with `default = false` — no variables.tf changes were needed.