Skip to content

DND-1537: export RDS error/slowquery logs to CloudWatch, with retention - #69

Merged
darenjacobs merged 5 commits into
mainfrom
darenjacobs/DND-1537/rds-cloudwatch-logs-exports
Aug 13, 2026
Merged

darenjacobs merged 5 commits into
mainfrom
darenjacobs/DND-1537/rds-cloudwatch-logs-exports

Conversation

@darenjacobs

@darenjacobs darenjacobs commented Aug 13, 2026 •

Copy link
Copy Markdown

User description

Why

A fleet audit for DND-1537 found EnabledCloudwatchLogsExports null on all 16 STSaaS Aurora clusters. The module never had the attribute at all, so there was no per-env knob to set — this was not a per-environment oversight.

That gap blocked the CUST-6816 post-mortem. agentro is granted rds:DescribeDBLogFiles but not rds:DownloadDBLogFilePortion, so the investigation could see that the reader's error log was 254 KB in the failure hour against a ~100 KB/hour baseline — and could not read a byte of it. Exporting to CloudWatch Logs closes that without widening the read-only IAM role, which was the alternative considered and rejected.

What

Variable Default
rds_enabled_cloudwatch_logs_exports ["error", "slowquery"]
rds_log_retention_days 90

Wired into aws_rds_cluster.enabled_cloudwatch_logs_exports, plus an aws_cloudwatch_log_group per exported type.

Why the log groups are created here rather than left to RDS

RDS auto-creates /aws/rds/cluster/<id>/<type> at "never expire" the instant an export is enabled — verified live while enabling this on zoox. Two consequences:

  1. That is exactly how the pre-rebuild zoox slowquery group came to hold 3.65 GB of dead data indefinitely (last event 2026-04-01, deleted under DND-1537).
  2. If RDS creates the group first, terraform collides with an unmanaged resource — an import to do 16 times over.

depends_on makes the ordering explicit: groups exist, with retention, before the export switches on.

Cost

Measured, not estimated — describe-db-log-files reports raw sizes, which is what CloudWatch bills ingestion on. Across all 30 instances over a 30-day retained window:

  • 0.093 GB/day fleet-wide raw log generation
  • ~$1.39/month ingestion, ~$1.47/month with 90-day retention, for all 16 clusters

Note on slowquery

It produces nothing until slow_query_log=1 is also set via rds_cluster_parameters, which is off on 15 of 16 clusters today (bmw is the exception, at long_query_time=5). The export is enabled anyway so the group exists and is retention-managed from the moment that parameter is turned on.

Enabling the parameter is deliberately not in this PR: the slow query log records full SQL text including literals — workspace and project names, IDs, user-supplied strings — so it needs a data-governance decision for the regulated single-tenant customers before that text is shipped into CloudWatch. Tracked separately on DND-1537.

Testing

  • terraform fmt -check -recursive — clean
  • terraform validate — passes (remaining warnings are pre-existing name/region deprecations)

Rollout note

Envs are pinned v1.20.6–v2.1.0 against main at v5.6.0, so this reaches new environments on the next release; existing clusters need either a module migration or direct enablement. zoox has already been enabled directly and is confirmed working — error log content is now readable, and RDS backfilled ~2 weeks of history on enablement.

🤖 Generated with Claude Code


Generated description

Below is a concise technical summary of the changes proposed in this PR:
Enable Aurora MySQL error and slow-query log exports to CloudWatch Logs through the RDS module, creating retention-managed log groups before RDS begins exporting. Expose configurable export types, retention, KMS encryption, cluster identifiers, and log group names/ARNs for operations and adoption.

TopicDetails
Log group outputs Expose log group names and ARNs at the module and root levels so existing out-of-band resources can be imported and downstream monitoring integrations can reference them.
Modified files (2)
  • modules/comet_rds/outputs.tf
  • outputs.tf
Latest Contributors(2)
UserCommitDate
darenjacobs@msn.comdecouple log-group lif...August 13, 2026
alexb@comet.comfeat: configurable AWS...August 11, 2026
RDS log exports Configure Aurora clusters to export supported MySQL logs and create matching CloudWatch log groups ahead of time, with finite retention, optional KMS encryption, lifecycle protection, and validation that every export has a managed group.
Modified files (4)
  • main.tf
  • modules/comet_rds/main.tf
  • modules/comet_rds/variables.tf
  • variables.tf
Latest Contributors(2)
UserCommitDate
darenjacobs@msn.comenforce exports subset...August 13, 2026
alexb@comet.comfeat(comet_secretsmana...August 12, 2026
Review this PR on Baz | Customize your next review

A fleet audit for DND-1537 found EnabledCloudwatchLogsExports null on all 16
STSaaS Aurora clusters. The module never had the attribute, so there was no
per-env knob to set — this was not an oversight per environment.

That gap blocked the CUST-6816 post-mortem: agentro is granted
rds:DescribeDBLogFiles but not rds:DownloadDBLogFilePortion, so the
investigation could see that the reader's error log was 254 KB in the failure
hour against a ~100 KB/hour baseline, and could not read a byte of it.
Exporting to CloudWatch Logs closes that without widening the read-only role.

Adds:

  rds_enabled_cloudwatch_logs_exports  default ["error", "slowquery"]
  rds_log_retention_days               default 90

The log groups are created explicitly rather than left to RDS. RDS auto-creates
/aws/rds/cluster/<id>/<type> at "never expire" the instant an export is enabled
— that is how the pre-rebuild zoox slowquery group came to hold 3.65 GB of dead
data indefinitely, and it would leave 16 clusters' worth of groups unmanaged and
needing import. depends_on makes the ordering explicit so the groups exist, with
retention, before the export switches on.

Measured cost across the whole fleet: 0.093 GB/day of raw log generation, so
~$1.39/month ingestion and ~$1.47/month with 90-day retention.

Note "slowquery" produces nothing until slow_query_log=1 is also set via
rds_cluster_parameters, which is off on 15 of 16 clusters today. It is enabled
here anyway so the group exists and is retention-managed from the moment that
parameter is turned on. Enabling the parameter itself is deliberately not part
of this change: the slow query log records full SQL text including literals, so
it needs a data-governance decision for the regulated single-tenant customers
first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@darenjacobs
darenjacobs requested a review from a team as a code owner August 13, 2026 19:27
Review follow-ups on this PR.

skip_destroy on the log groups. Dropping a type from
rds_enabled_cloudwatch_logs_exports removes it from the for_each, which would
otherwise destroy the group and every log in it — turning an export off must not
delete the evidence already collected. The cost is that a real teardown leaves
the groups behind, but they carry retention now and expire on their own, unlike
the never-expire orphan DND-1537 had to clean up.

Root-level validation on both variables. The inner module already validated
them, so this is not a behaviour change; it surfaces a bad value at the env
config rather than one layer deeper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread modules/comet_rds/main.tf
Comment on lines +80 to +91
resource "aws_cloudwatch_log_group" "rds_exported_logs" {
for_each = toset(var.rds_enabled_cloudwatch_logs_exports)

name = "/aws/rds/cluster/${local.rds_cluster_identifier}/${each.value}"
retention_in_days = var.rds_log_retention_days == 0 ? null : var.rds_log_retention_days

# Dropping a type from rds_enabled_cloudwatch_logs_exports removes it from the for_each,
# which would otherwise destroy the group and every log in it. Turning an export off must
# not delete the evidence already collected — that is the whole point of exporting.
# The cost is that a real teardown leaves the groups behind, but they carry retention now
# and expire on their own, unlike the never-expire orphan DND-1537 had to clean up.
skip_destroy = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aws_cloudwatch_log_group.rds_exported_logs's for_each attempts to create /aws/rds/cluster/<id>/<type> groups that RDS may already own, so CreateLogGroup fails with ResourceAlreadyExistsException and blocks apply; with skip_destroy = true, removing and re-adding an export key repeats the conflict — should we add an adoption migration using exact terraform import addresses or make re-adoption lifecycle-safe?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
modules/comet_rds/main.tf around lines 80-91, the
`aws_cloudwatch_log_group.rds_exported_logs` `for_each` resource unconditionally tries
to create `/aws/rds/cluster/<id>/<type>` log groups, which fails with
`ResourceAlreadyExistsException` when RDS has already created them (as is the case for
already-enabled clusters like zoox), and also fails when an export is disabled then
re-enabled, since `skip_destroy = true` retains the AWS group while Terraform removes it
from state. Add a state-adoption/import mechanism for existing groups — such as
Terraform import blocks or documented `terraform import` commands using the exact
`for_each` addresses — so existing groups are adopted rather than recreated, while
still allowing normal creation for groups that don't exist yet. Ensure the fix supports
the disable/re-enable cycle without manual intervention and doesn't unconditionally
import groups that are genuinely missing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit b23500 addressed this comment by decoupling log-group management from export toggles, so disabling and re-enabling an export retains the resource in Terraform state and avoids recreation conflicts. It does not add adoption/import handling for groups that already exist outside Terraform.

Comment thread variables.tf Outdated
Comment on lines +1187 to +1194
variable "rds_log_retention_days" {
description = "Retention for the RDS CloudWatch log groups, in days. 0 means keep forever — avoid: that is what RDS applies on its own when it auto-creates the groups, and the reason DND-1537 found an orphaned group holding 3.65 GB of dead data indefinitely."
type = number
default = 90

validation {
condition = contains([0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653], var.rds_log_retention_days)
error_message = "Must be a retention period CloudWatch Logs accepts (0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653)."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rds_log_retention_days accepts 0, which modules/comet_rds/main.tf converts to retention_in_days = null, so enabled-by-default slowquery and other exported database logs lack a CloudWatch retention policy and can retain SQL text indefinitely — should we reject 0 for sensitive exports or require a finite, centrally approved retention period?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In `variables.tf`
around lines 1187-1194, update the `rds_log_retention_days` validation so `0` cannot be
configured when CloudWatch database log exports (especially `slowquery`) are enabled,
preferably by removing the zero-retention sentinel and requiring a finite supported
retention period, or by adding a conditional validation tied to
`rds_enabled_cloudwatch_logs_exports` with a clear error message. Align the variable
description and error message with this policy, and ensure the downstream conversion to
`retention_in_days = null` in `modules/comet_rds/main.tf` cannot produce indefinitely
retained logs for `error`, `slowquery`, or other exports.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit b23500b addressed this comment by removing 0 from the allowed retention values and explicitly requiring a finite CloudWatch retention period. It also creates managed log groups with that retention before enabling RDS exports.

@jms200 jms200 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloned the branch and ran terraform init / validate against hashicorp/aws v6.60.0 — the AWS provider, not a module tag — plus a schema and repo-convention check. That's what this repo's unbounded >= 6.52 floor resolves to today, and it matches what comet-devops (~> 6.50, != 6.57.0) resolves to on the Atlantis path, so it's the version migrated envs would actually apply this under.

Verdict: sound change, correct implementation, unusually well-justified — the measured cost figure and the live zoox verification make this easy to review. Two findings I'd like a response on before merge; neither is a correctness bug, but #2 gets baked into 16 environments if it ships as-is.

Verified

  • terraform validate passes on the branch. The only warnings are the pre-existing name/region deprecations inside vendored upstream modules (terraform-aws-modules/eks) — nothing from this PR.
  • skip_destroy is not deprecated on aws_cloudwatch_log_group in AWS provider 6.60 (checked terraform providers schema directly — I'd half-expected it to be). Safe against the hashicorp/aws >= 6.52 floor in versions.tf.
  • /aws/rds/cluster/<id>/<type> is the correct Aurora MySQL group shape, and hoisting rds_cluster_identifier into a local is the right fix for the name-drift risk. That's the detail most implementations of this get wrong.
  • The log-type validation set (audit/error/general/slowquery) is correct for Aurora MySQL, with no Postgres false-reject risk in practice — the module is MySQL-only (hardcoded port 3306, aurora-mysql${version} parameter-group family).
  • retention_in_days = 0 ? null is right; the provider treats both as never-expire and null is the cleaner spelling. Retention values list matches CloudWatch's accepted set exactly.
  • depends_on creates no cycle, since the group name derives from the local rather than from the cluster resource.
  • Root gating is fine — count = var.enable_rds ? 1 : 0 (main.tf:402) means RDS-less envs create no groups.

1. skip_destroy makes disabling an export a one-way door

modules/comet_rds/main.tf:91

Dropping a type from rds_enabled_cloudwatch_logs_exports removes it from the for_each — gone from state, still in AWS. Re-enabling it later fails on ResourceAlreadyExistsException. That's the same collision this PR exists to prevent, just deferred and self-inflicted.

The root cause is that one variable drives two lifecycles. Decoupling them fixes it cleanly: for_each the groups over a separate rds_managed_log_group_types (default ["error", "slowquery"]), and let rds_enabled_cloudwatch_logs_exports control only the cluster attribute. Then toggling an export off never removes a group from state, skip_destroy becomes belt-and-braces rather than load-bearing, and re-enabling is a no-op.

If you'd rather not add a variable, the minimum is documenting the re-enable import in the variable description alongside the disable caveat that's already there.

2. No kms_key_id on the log groups — the one I'd most want in this PR

The cluster takes rds_kms_key_id and Performance Insights takes rds_performance_insights_kms_key_id; the new groups have no equivalent knob, so they land on the CloudWatch service-managed key.

The PR's own argument is that the slow query log carries customer SQL text — workspace names, project names, user-supplied literals — and needs a data-governance decision for the regulated single-tenant customers. CloudWatch encryption is exactly where that decision lands. Adding rds_log_kms_key_id (default null, no behaviour change) now is cheap; retrofitting it after this reaches all 16 clusters means touching every env a second time, right at the moment DND-1537 flips slow_query_log=1.

Trivy will also flag this (AVD-AWS-0017). Report-only today, but ci.yml states the intent to make it blocking once the existing findings are triaged — no reason to add to that backlog.

3. No outputs for the new groups

modules/comet_rds/outputs.tf exports endpoints and cluster id but not the log group names or ARNs. The rollout explicitly needs those names for terraform import on every cluster enabled out-of-band, and alarms / metric filters will want the ARNs. mysql_log_group_names / mysql_log_group_arns would make the import command derivable rather than hand-assembled up to fifteen times.

4. skip_destroy survives a real teardown, and nothing tracks the orphans

Acknowledged in the description, and retention bounds the damage — unlike the never-expire orphan DND-1537 had to clean up. But STSaaS offboarding is a live workflow, so this is worth a line in the decommission checklist rather than only in a PR body.

Nits

  • The README Inputs table isn't regenerated. It's terraform-docs-shaped but already covers 55 of 245 variables, and #63/#64/#65 didn't update it either — so this matches convention and isn't a blocker. Separately: there's no terraform_docs hook in .pre-commit-config.yaml, so that table drifts unchecked, which is arguably worse than having no table.
  • Root and module validation blocks are byte-identical duplicates. Deliberate per the commit message and fine — just noting it as a drift point if CloudWatch ever adds a retention value.
  • Cost math holds: ingestion is 0.093 GB/day × 30 × $0.50 ≈ $1.40, and the $0.07 storage delta implies the compressed-archive billing basis. Right order of magnitude either way.

One addition to the rollout note

Enabling enabled_cloudwatch_logs_exports is an in-place cluster modify — no reboot, no downtime. With apply_immediately = true already set (modules/comet_rds/main.tf:123), envs adopting this take it live on apply. Worth stating explicitly so nobody schedules a maintenance window they don't need.

…+ outputs

Review findings from @jms200 on #69.

1. skip_destroy made disabling an export a one-way door. Dropping a type from
rds_enabled_cloudwatch_logs_exports removed it from the for_each — gone from
state, still in AWS — so re-enabling it later would fail on
ResourceAlreadyExistsException. That is the same collision this change exists to
prevent, deferred and self-inflicted, and it was introduced by the skip_destroy
fix earlier in this PR rather than being in the original design.

Fixed as suggested, by splitting the two lifecycles: the log groups now for_each
over a new rds_managed_log_group_types, and rds_enabled_cloudwatch_logs_exports
controls only the cluster attribute. Toggling an export is now purely a cluster
modify and never touches group state. A type managed but not exported yields an
empty group with retention already applied, which is exactly the desired state
for slowquery on the 15 clusters where slow_query_log=0. skip_destroy stays as
belt-and-braces.

2. Added rds_log_kms_key_id (default null, no behaviour change). The cluster and
Performance Insights both take a key and the groups did not. Cheap now; after
this reaches 16 clusters it means touching every env a second time, at exactly
the moment slow_query_log=1 starts putting customer SQL text in these groups.
Also what trivy AVD-AWS-0017 asks for.

3. Added mysql_log_group_names / mysql_log_group_arns outputs, at module and root.
The rollout needs the names to build terraform import addresses for every cluster
enabled out-of-band, and alarms and metric filters will want the ARNs.

Also, from the Baz review: rds_log_retention_days no longer accepts 0. Never-expire
is what RDS applies on its own and the reason DND-1537 found a 3.65 GB orphan; a
module whose purpose is preventing that should not offer it. These groups carry
customer SQL text once slow_query_log is on. 3653 days remains available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@darenjacobs

Copy link
Copy Markdown
Author

Thanks — findings 1–3 addressed in b23500b, plus the Baz retention point. Responses below.

1. skip_destroy one-way door — fixed, and you found my own bug

You're right, and worth noting where it came from: skip_destroy wasn't in the original design. It was added a commit earlier in this same PR, in response to a self-review finding that dropping a type would delete the group and its logs. That fix traded a delete for a state/AWS divergence — strictly better, but it introduced exactly the collision this PR exists to prevent.

Implemented your suggestion rather than the documentation fallback, since the variable is cheap and the failure mode is the expensive kind:

for_each          = toset(var.rds_managed_log_group_types)   # groups
enabled_cloudwatch_logs_exports = var.rds_enabled_cloudwatch_logs_exports   # cluster only

Toggling an export is now purely a cluster modify and never touches group state, so re-enabling is a no-op. skip_destroy stays as belt-and-braces — it now only bites when a type is removed from rds_managed_log_group_types, which is an explicit "stop managing this group" rather than a side effect.

Useful side effect for the rollout: a type managed but not exported yields an empty group with retention already applied. That is exactly the desired state for slowquery on the 15 clusters where slow_query_log=0 — the group is sitting there correctly configured for the moment DND-1537's follow-up flips the parameter.

2. kms_key_id — added

Agreed, and your timing argument is the decisive one. rds_log_kms_key_id, default null, no behaviour change. Retrofitting after this reaches 16 clusters would mean touching every env a second time at precisely the moment those groups start carrying customer SQL text.

3. Outputs — added

mysql_log_group_names and mysql_log_group_arns, keyed by log type, at both module and root (root gated on enable_rds). The import address is now derivable instead of hand-assembled:

terraform import \
  'module.comet.module.comet_rds.aws_cloudwatch_log_group.rds_exported_logs["error"]' \
  "$(terraform output -json mysql_log_group_names | jq -r '.error')"

4. Teardown orphans — agreed, not this repo

The decommission checklist lives in the /teardown-stsaas skill in comet-support-agent, so it can't ride this PR. Noted on DND-1549, which owns the rollout, so it lands with the work that creates the orphans rather than as a loose follow-up.

Baz: rds_log_retention_days = 0

Took this one. 0 is no longer accepted. It is what RDS applies when it creates the groups itself and the direct cause of the 3.65 GB orphan DND-1537 cleaned up — a module whose stated purpose is preventing that shouldn't offer it as a reachable option, least of all on groups that will carry customer SQL text. 3653 (10 years) remains available if something genuinely needs long retention.

Baz: adoption of pre-existing groups

Real, and already true of zoox. Not fixable inside the module — import blocks need the concrete cluster identifier, which is per-env. Handled by the outputs above plus the import command in the PR description, and it is an explicit acceptance criterion on DND-1549. The disable/re-enable half of that finding is resolved by finding 1.

Nits

CI green on b23500b.

@baz-reviewer

baz-reviewer Bot commented Aug 13, 2026

Copy link
Copy Markdown

Thanks for the detailed update and rationale. I can only save feedback to memory for specific code review findings, not general PR-level feedback.

jms200
jms200 previously approved these changes Aug 13, 2026

@jms200 jms200 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Re-checked b23500b on a fresh clone rather than reading the summary: terraform fmt -check -recursive clean, terraform validate passes (only the pre-existing upstream name/region deprecations), all four CI jobs green.

All three findings are actually implemented, and the decoupling is the right shape:

  • 1 — one-way door. for_each now runs off rds_managed_log_group_types, the cluster attribute off rds_enabled_cloudwatch_logs_exports. Toggling an export is a pure cluster modify that never touches group state, so re-enabling is a no-op. Your point about the managed-but-not-exported case is the better half of this: slowquery on the 15 clusters at slow_query_log=0 now sits there as an empty group with retention already applied, which is exactly the state DND-1537's follow-up wants to walk into.
  • 2 — KMS. rds_log_kms_key_id, default null. No behaviour change, knob exists before it reaches 16 clusters.
  • 3 — outputs. Keyed by log type at both levels, root gated on enable_rds. Import address is derivable now.

Good catch removing the dead == 0 ? null ternary along with 0 from the validation set — those had to move together, and it would have been easy to drop one and leave the other.

No objection to the two you pushed back on. Root-level validation failing at the env config rather than inside an Atlantis plan frame is worth the duplication, and hand-regenerating a terraform-docs table with no hook to keep it honest just drifts again — the missing hook is the real fix.

Two non-blocking notes for whoever picks up DND-1549, neither worth another round here:

Nothing enforces rds_enabled_cloudwatch_logs_exports ⊆ rds_managed_log_group_types. Both descriptions say "should be a superset" and both default to the same list, so it's correct out of the box — but setting exports to include, say, general without adding it to the managed list puts RDS back in charge of creating that group, at never-expire. That's this PR's own failure mode reintroduced through a new door, and by the standard you applied to retention_in_days = 0 it's the kind of thing worth making unreachable rather than documenting.

Cross-variable references in validation blocks need Terraform 1.9, above this repo's >= 1.5.7 floor. A lifecycle.precondition on aws_rds_cluster does the same job at the current floor:

lifecycle {
  precondition {
    condition = alltrue([
      for t in var.rds_enabled_cloudwatch_logs_exports :
      contains(var.rds_managed_log_group_types, t)
    ])
    error_message = "Every exported log type must also be in rds_managed_log_group_types, or RDS creates its log group at never-expire."
  }
}

KMS key policy. When someone first sets rds_log_kms_key_id, the key policy has to grant logs.<region>.amazonaws.com — otherwise CreateLogGroup fails with InvalidParameterException, which reads like a terraform problem and isn't. Default null means nobody hits it today; worth a line in the variable description before the first regulated env turns it on.

Comment on lines +35 to +37
output "mysql_log_group_names" {
description = "CloudWatch log group names for the exported RDS logs, keyed by log type. Use when adopting a cluster whose export was enabled out-of-band: terraform import 'module.<path>.aws_cloudwatch_log_group.rds_exported_logs[\"error\"]' <name>"
value = { for t, lg in aws_cloudwatch_log_group.rds_exported_logs : t => lg.name }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mysql_log_group_names/mysql_log_group_arns derive from aws_cloudwatch_log_group.rds_exported_logs keyed by rds_managed_log_group_types, so groups like slowquery appear as active exports when slow_query_log is off. Should we filter them by rds_enabled_cloudwatch_logs_exports, or rename them as managed-group lists and expose a separate active-export list for metric filters, subscriptions, and alarms?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`modules/comet_rds/outputs.tf` around lines 35-42, `mysql_log_group_names` and
`mysql_log_group_arns` currently expose every `rds_managed_log_group_types` entry from
`aws_cloudwatch_log_group.rds_exported_logs`, including groups not enabled for export
(e.g. `slowquery` when `slow_query_log` is off). Either refactor these outputs to
build/filter an active-export map keyed by `rds_enabled_cloudwatch_logs_exports`, or
rename the `rds_exported_logs` resource/address and these outputs to managed-log-group
terminology (updating descriptions and any ARN output accordingly) and provide the
active export list as a separate output for metric filters, subscriptions, and alarms.

Comment thread variables.tf
}

variable "rds_managed_log_group_types" {
description = "MySQL log types whose CloudWatch log groups are created and retention-managed here. Kept separate from rds_enabled_cloudwatch_logs_exports so that disabling an export is not a one-way door: the group stays in state, and re-enabling later is a no-op instead of a ResourceAlreadyExistsException. Should be a superset of the export list."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported logs bypass retention management

rds_enabled_cloudwatch_logs_exports = ["audit"] is accepted even when rds_managed_log_group_types excludes audit, so RDS can create the group with Never Expire retention and bypass this module’s retention and KMS configuration — should we require every enabled export in rds_enabled_cloudwatch_logs_exports to be contained in var.rds_managed_log_group_types, including in the child module?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In `variables.tf`
around lines 1188-1194, update the
`rds_managed_log_group_types`/`rds_enabled_cloudwatch_logs_exports` validation logic to
require every enabled export to be present in the managed log-group list, preventing RDS
from auto-creating unmanaged groups. Apply the equivalent cross-variable validation in
the child module’s corresponding variable configuration, with a clear error message
identifying the missing managed log type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 59e3a18 addressed this comment by adding a child-module precondition requiring every enabled export to appear in rds_managed_log_group_types, preventing unmanaged RDS-created log groups.

…policy

Both non-blocking notes from @jms200's approving review. Cheap enough to take
now rather than leave for DND-1549.

Exporting a type with no managed log group hands group creation back to RDS,
which makes it at never-expire — this module's own failure mode through a new
door. Both variables default to the same list so it is correct out of the box,
but by the same standard applied to retention 0, it should be unreachable rather
than documented. Implemented as a lifecycle.precondition rather than a
cross-variable validation block: the latter needs Terraform 1.9 and this repo's
floor is >= 1.5.7.

Also documents that a KMS key policy must grant logs.<region>.amazonaws.com
before rds_log_kms_key_id can be set, since CreateLogGroup otherwise fails with
InvalidParameterException, which reads like a terraform fault and is not.
Default null means nobody hits this today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@darenjacobs

Copy link
Copy Markdown
Author

Thanks for the approval. Took both non-blocking notes anyway in 59e3a18 — they were cheap, and the repo has no branch protection so this doesn't cost the approval.

Subset enforcement. You're right that documenting it was inconsistent with removing 0 from the retention set — same class of footgun, and it reintroduces this PR's own failure mode through a new door. Implemented as your lifecycle.precondition; confirmed the required_version floor is >= 1.5.7, so a cross-variable validation block genuinely isn't available.

KMS key policy. Added to both variable descriptions. Worth having written down precisely because InvalidParameterException on CreateLogGroup reads like a terraform fault — whoever turns this on for the first regulated env would otherwise lose time to it.

fmt and validate clean locally; CI re-running.

Leaving the merge to you, since DND-1549 sequencing (migrate vs. enable-directly, and the zoox import) is the decision that determines when any of this actually reaches a cluster.

…ypes

Baz finding on b23500b, and a genuine defect I introduced: the outputs were added
when the log groups were keyed by the export list, and their descriptions still
said "for the exported RDS logs". Decoupling the two lifecycles made that false —
they are keyed by rds_managed_log_group_types now, so `slowquery` appears in them
on all 15 clusters where slow_query_log=0 and nothing is written.

Keeping them keyed by managed types is correct for their primary purpose: import
needs every group under management, including an empty one. So the fix is to say
so, and to give consumers the filter they actually need — a metric filter or alarm
attached to the slowquery group today would sit on a group nothing writes to.

Adds mysql_exported_log_types (the cluster's live enabled_cloudwatch_logs_exports)
at module and root, and rewords both existing outputs to state that they are a
superset.

Note a type can be exported and still produce nothing: slowquery stays empty until
slow_query_log=1 is set via rds_cluster_parameters, which is DND-1549's problem 2.
The output cannot express that, so both descriptions say it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@darenjacobs

Copy link
Copy Markdown
Author

Baz findings — status

Four raised, all now resolved. Two Baz confirmed itself, one is deliberate, one was a real defect I'd introduced.

# Finding Status
1 ResourceAlreadyExistsException on pre-existing groups partly fixed in b23500b; adoption is deliberate — see below
2 rds_log_retention_days accepts 0 ✅ fixed b23500b (Baz confirmed)
3 Outputs present slowquery as an active export ✅ fixed 9adec86
4 Exports can bypass managed groups ✅ fixed 59e3a18 (Baz confirmed)

3 — real defect, and mine

Baz is right, and it was a regression from my own decoupling fix. The outputs were written when the groups were keyed by the export list, and their descriptions still said "for the exported RDS logs". Once for_each moved to rds_managed_log_group_types that became false: slowquery now appears in them on all 15 clusters where slow_query_log=0 and nothing is written. Anyone attaching a metric filter or alarm off mysql_log_group_arns would have targeted a group nothing writes to.

Took the "rename + separate list" branch rather than filtering, because filtering would break the outputs' primary purpose — terraform import needs every managed group, including an empty one:

  • both existing outputs now say MANAGED, and state they are a superset
  • new mysql_exported_log_types exposes the cluster's live enabled_cloudwatch_logs_exports to filter by

Worth noting the residual sharp edge the outputs can't express: a type can be exported and still produce nothing. slowquery is exported on zoox right now and its group doesn't exist, because slow_query_log=0. Both descriptions say so explicitly.

1 — adoption stays deliberate

Baz's follow-up is accurate: b23500b fixed the disable/re-enable half but "does not add adoption/import handling for groups that already exist outside Terraform." That's intentional and not fixable in the module — import blocks need the concrete cluster identifier, which is per-env. It's handled by mysql_log_group_names (making the address derivable), the import command in the description, and an explicit acceptance criterion on DND-1549, which owns the rollout that creates the condition.

Only zoox is affected today, and only because it was enabled out-of-band this afternoon.

CI green on 9adec86. @jms200 — approval predates these last two commits (59e3a18, 9adec86); both are narrow and address your notes plus Baz #3, but re-skim if you'd rather.

@darenjacobs
darenjacobs requested a review from jms200 August 13, 2026 20:40
@darenjacobs

Copy link
Copy Markdown
Author

@jms200 re-review requested — your approval was dismissed by two pushes after it, and that's my error.

I checked /branches/main/protection, got 404 Branch not protected, and concluded extra commits were free. Wrong API: this repo uses a ruleset (main-protection, active) with dismiss_stale_reviews_on_push: true, which the classic branch-protection endpoint doesn't report. Had I checked /rulesets I'd have asked first rather than pushing.

Two commits landed after your approval, both narrow:

Commit What
59e3a18 Your two non-blocking notes — lifecycle.precondition for the subset rule, KMS key-policy caveat in the variable descriptions
9adec86 Baz #3 — outputs said "exported" but were keyed by managed types after the decoupling. Reworded to MANAGED, added mysql_exported_log_types

9adec86 is the only one you haven't seen the substance of. It's a real defect I introduced: the outputs were written when the groups were keyed by the export list, so once for_each moved to rds_managed_log_group_types the descriptions were false — slowquery appears in them on all 15 clusters where nothing is written, and an alarm wired off mysql_log_group_arns would target a dead group. Kept them keyed by managed types (import needs every group, including empty ones) and added the export list as a separate output to filter by.

fmt, validate, tflint, trivy all green on 9adec86.

Diff since your approval if it's easier than a full re-read:

git diff 59e3a18^..9adec86 -- modules/comet_rds/ outputs.tf variables.tf

Comment on lines +173 to +177
type = list(string)
default = ["error", "slowquery"]

validation {
condition = alltrue([for t in var.rds_enabled_cloudwatch_logs_exports : contains(["audit", "error", "general", "slowquery"], t)])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct module null input crashes

var.rds_enabled_cloudwatch_logs_exports and var.rds_managed_log_group_types remain nullable, so callers can pass null and validation or resource expansion fails before the resource precondition emits a controlled diagnostic — should we set nullable = false or normalize both inputs to [] before validation and use?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
modules/comet_rds/variables.tf around lines 173-177, update
rds_enabled_cloudwatch_logs_exports so callers cannot pass null and trigger a failure
while its validation iterates the value. Apply the same nullable=false boundary to
rds_managed_log_group_types around lines 193-197, preventing its validation and
downstream toset/resource expansion from receiving null. Preserve the existing list
defaults and validation behavior, and update any relevant tests or documentation if
needed.

Comment on lines +191 to +194
variable "rds_managed_log_group_types" {
description = "MySQL log types whose CloudWatch log groups this module creates and manages retention for. Should be a superset of rds_enabled_cloudwatch_logs_exports — a type listed here but not exported simply yields an empty group with retention already applied, ready for when the export (or slow_query_log) is switched on. Removing a type here stops managing its group; it is NOT deleted, because of skip_destroy."
type = list(string)
default = ["error", "slowquery"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing a type from rds_managed_log_group_types or setting enable_rds = false removes its for_each instance from Terraform state, while skip_destroy retains /aws/rds/cluster/<id>/<type> in AWS, so re-adding it fails on CreateLogGroup with ResourceAlreadyExistsException — should we provide an adoption/import path or stable resource addressing instead of relying on manual import?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`modules/comet_rds/variables.tf` around lines 191-194, and in the
`aws_cloudwatch_log_group.rds_exported_logs` resource that consumes
`rds_managed_log_group_types`, fix the lifecycle mismatch where removing (or re-adding)
a type, or setting `enable_rds = false`, drops the Terraform state entry while
`skip_destroy` leaves the AWS log group orphaned. Refactor so remove/re-add cycles are
safe—either retain the resource state with an explicit protected-removal workflow, use
a stable non-shrinking set of resource addresses, or require an explicit state/import
migration—then update the variable description and adoption guidance to match the
chosen behavior. Add or update coverage for remove/re-add and `enable_rds = false`
scenarios so they cannot fail with `ResourceAlreadyExistsException`.

Comment thread variables.tf
Comment on lines +1176 to +1179
variable "rds_enabled_cloudwatch_logs_exports" {
description = "MySQL log types the CLUSTER exports to CloudWatch Logs. 'error' carries the entries that matter for post-mortems. 'slowquery' produces nothing unless slow_query_log=1 is also set via rds_cluster_parameters — it is enabled here so the log group exists and is retention-managed from the moment that parameter is turned on. Pass [] to stop exporting; the log groups are governed separately by rds_managed_log_group_types, so disabling an export never removes a group from state."
type = list(string)
default = ["error", "slowquery"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unapproved SQL export enabled by default

The default rds_enabled_cloudwatch_logs_exports enables slowquery for every RDS deployment, and arbitrary rds_cluster_parameters name/value pairs can make slow_query_log=1 effective, so customer SQL text reaches CloudWatch without any data-governance approval or authorized-consumer gate — should we require explicit approval for slow_query_log or drop slowquery from the default until that gate exists?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In `variables.tf`
around lines 1176-1179, update the `rds_enabled_cloudwatch_logs_exports` default so
`slowquery` is not enabled for every deployment. Because `rds_cluster_parameters` can
enable `slow_query_log` through arbitrary name/value pairs, require an explicit,
documented opt-in or approval gate before allowing `slowquery` export; otherwise default
to `['error']` and retain `slowquery` only when intentionally configured.

Comment thread variables.tf
Comment on lines +1204 to +1207
variable "rds_log_retention_days" {
description = "Retention for the RDS CloudWatch log groups, in days. Must be finite — 'never expire' is intentionally not offered: that is what RDS applies when it creates the groups itself, and the reason DND-1537 found an orphan holding 3.65 GB of dead data indefinitely."
type = number
default = 90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical RDS logs are deleted after adoption

The new root default applies retention_in_days = 90 to imported pre-existing /aws/rds/cluster/<id>/<type> groups, so CloudWatch Logs permanently deletes post-mortem events older than 90 days, including those predating adoption. Should the migration require an explicit retention/archive decision and document consumer/customer commitments before applying 90 days to existing groups?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In `variables.tf`
around lines 1204-1207, update the `rds_log_retention_days` configuration and its
downstream managed-log-group logic so existing CloudWatch log groups are not silently
changed to 90-day retention. Require an explicit retention/archive opt-in for applying
retention to pre-existing groups, and when unset, preserve their current retention
rather than issuing a destructive update; adjust validation and document the migration
decision required before enabling it.

@jms200 jms200 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving. Checked 59e3a18 and 9adec86 on a fresh clone: fmt -check -recursive clean, validate passes with only the pre-existing upstream warnings, all four CI jobs green on 9adec86.

No concerns about the dismissal — that was the ruleset doing its job, and both commits are narrow.

59e3a18. Precondition sits in the existing lifecycle block on aws_rds_cluster and the condition is right. Worth noting the property that makes it the right mechanism rather than just an available one: it references only input variables, so Terraform evaluates it at plan time. A bad export list fails the plan outright instead of surfacing partway through an apply with some groups already created. That's the behaviour a cross-variable validation block would have given, without the 1.9 floor.

KMS caveat reads well in both descriptions.

9adec86. Baz was right and your diagnosis of it is right — after the decoupling, outputs keyed by rds_managed_log_group_types while describing themselves as "the exported RDS logs" were straightforwardly false, and an alarm wired off mysql_log_group_arns would have targeted a slowquery group nothing writes to on 15 clusters.

Keeping them keyed by managed types and adding the export list alongside is the correct branch. Filtering would have fixed the alarm case by breaking the import case, which is the outputs' primary reason to exist. And mysql_exported_log_types reading aws_rds_cluster.cometml-db-cluster.enabled_cloudwatch_logs_exports rather than the input variable is the detail that makes it trustworthy — it reflects what the cluster actually carries, so it stays honest if the two ever drift.

Calling out the exported-but-empty case explicitly in the description is the right call too. That one can't be expressed structurally — slowquery is exported on zoox today and its group doesn't exist — so documenting it is the only option, and it's the thing most likely to waste someone's afternoon.

Agreed on leaving finding 1 as deliberate: import blocks need a per-env cluster identifier, so it isn't module-shaped. Derivable addresses plus an acceptance criterion on DND-1549 is the right split.

Nothing further from me. Merge sequencing is yours — as you say, DND-1549 decides when this reaches a cluster, and the zoox import has to land with it.

@darenjacobs
darenjacobs merged commit e409429 into main Aug 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants