Skip to content

feat(cost): tune budgets, Config recording and log retention for cost control - #159

Open
Alexanderamiri wants to merge 2 commits into
mainfrom
feat/cost-guardrails
Open

Alexanderamiri wants to merge 2 commits into
mainfrom
feat/cost-guardrails

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

A cost audit (2026-10, live Cost Explorer/CloudWatch/Config data) found that the cost guardrails we already run have gaps:

  • The account budget fires every month. It is $500, but steady spend is about $550 a month (including 25% VAT), and the ACTUAL 80%/100% alerts trigger on that. There is no forecast alert.
  • javabin-billing-alarm (us-east-1) does nothing. It has been INSUFFICIENT_DATA since 2026-03-26 because the AWS/Billing EstimatedCharges metric is not enabled for the account. Its SNS topic has 0 subscribers, and its $200 threshold is below monthly spend anyway.
  • AWS Config churn. Over 30 days Config recorded about 12.6k configuration items ($0.003 each): NetworkInterface 2451, VPC 2446, Subnet 2446, SecurityGroup 2445. ENI + VPC + Subnet is about 7.3k items (~$22). Each Fargate task start/stop re-records the ENI and its relationships. Almost all of it comes from testteam-platform-test-app crash-looping (~80 starts/day), so fixing that service is the main saving; this PR only trims what remains.
  • The 15 /aws/lambda/javabin-* log groups and auth-claims never expire. Lambda auto-created them, and Terraform doesn't manage them.
  • The Config delivery bucket has no lifecycle rule.

What

  • cost-analytics:
    • Budget raised to $600.
    • Notifications are now FORECASTED >100%, ACTUAL >100% and ACTUAL >150%, all to javabin-alerts, which the slack-alert Lambda posts to #javabin-cost-alerts.
    • Removed aws_cloudwatch_metric_alarm.billing, aws_sns_topic.billing_alarm and the billing_alarm_threshold_usd variable. The us-east-1 provider alias stays because the CUR export uses it.
  • monitoring:
    • Config recorder still records all supported types, but AWS::EC2::NetworkInterface, AWS::EC2::VPC and AWS::EC2::Subnet use a DAILY override. Security groups stay CONTINUOUS. ENIs are deliberately not excluded: Security Hub EC2.180 (ENI source/destination check, enabled) evaluates AWS::EC2::NetworkInterface, and Config.1 fails when an enabled control's resource type is not recorded. Trade-off: EC2.180 evaluates daily instead of on every change, and an ENI created and deleted within one day may never be recorded. Expected saving ~$15–20/mo while the crash loop lasts, a few dollars a month after it is fixed.
    • The javabin-config-<acct> bucket now expires objects after 730 days. The bucket is not versioned.
  • lambdas/log-groups.tf and identity/log-groups.tf: aws_cloudwatch_log_group with 365-day retention for every platform Lambda.
  • imports.tf: for_each import blocks (TF 1.7) adopt the 14 existing groups plus auth-claims. budget-enforcer has never logged, so its group is created fresh.
  • scripts/tests/test_lambda_log_groups.py: scans every terraform/platform/**/*.tf and fails if a ${var.project}-* Lambda has no log group (shared local.platform_lambda_names or an explicit /aws/lambda/... group in its module).
  • Docs:
    • New docs/cost-management.md covering alerts, tags, CUR, built-in controls and why Config records ENIs daily, and the post-apply checks.
    • Updated CLAUDE.md (file table, a separate account-budget alert-routing line so it doesn't conflict with feat(monitoring): alert on ECS services that cannot start tasks #160, stale cost-allocation-tag note), docs/platform-modules.md and docs/lambda-functions.md.

Not changed, on purpose:

  • The anomaly monitor (SERVICE, $10 IMMEDIATE) is adequate.
  • Cost-allocation tags are already Active.
  • ECR lifecycle and app log retention (30 days) already exist.

Expected plan

  • module.cost_analytics.aws_budgets_budget.account: updated in place.
  • module.cost_analytics.aws_cloudwatch_metric_alarm.billing and aws_sns_topic.billing_alarm: destroyed. Both are inert, as explained above.
  • module.monitoring.aws_config_configuration_recorder.main: updated in place.
  • module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs: created.
  • 15 log groups imported (retention updated from never to 365 days) and 1 created (budget-enforcer).

How verified

  • terraform fmt -recursive -check, then terraform init -backend=false and terraform validate in terraform/platform. All pass.
  • python3 -m unittest discover -s scripts/tests (46 OK) and -s terraform/lambda-src/tests (OK). Negative check: renaming the auth-claims log group makes the guard test fail.
  • git merge-tree against feat/ecs-impaired-alert (feat(monitoring): alert on ECS services that cannot start tasks #160): no conflict.
  • Live: Config.1 currently FAILED for IAM global types + custom recorder role (pre-existing); EC2.180 rule scoped to AWS::EC2::NetworkInterface.
  • Live checks: the javabin-config-* bucket is unversioned with no lifecycle, the recorder is ALL_SUPPORTED_RESOURCE_TYPES + CONTINUOUS, and the list of existing /aws/lambda/javabin-* log groups matches the import list.

Follow-ups

  • 7 days after apply:
    • Check AWS/Config ConfigurationItemsRecorded per ResourceType. If SecurityGroup still dominates, add it to the DAILY override.
    • Security Hub EC2.180 still gets fresh evaluations (not NO_DATA).
    • Security Hub Config.1 has no new reason naming AWS::EC2::NetworkInterface.
  • Scale testteam-platform-test-app to 0, or re-push its image. It is the root cause of the Config churn.
  • Lower account_budget_usd as the legacy cleanup lands. The cleanup is in the cost audit report: idle EB environments, Default-ALB, Coolify, NAT.
  • The import blocks can be dropped after the first apply.
  • If the budget-enforcer Lambda runs before this is applied, its log group will already exist and the create will fail. In that case, add it to the import list.

🤖 Generated with Claude Code

… control

- Account budget $500 -> $600 (steady state ~$550 incl. VAT); notify on
  forecast >100%, actual >100%, actual >150% instead of a monthly 80% alert.
- Remove the inert EstimatedCharges billing alarm and its unsubscribed
  us-east-1 SNS topic (metric not enabled, INSUFFICIENT_DATA since 2026-03).
- AWS Config: stop recording EC2 network interfaces (Fargate churn), record
  VPC/Subnet daily (~$27/mo of configuration items).
- 730-day expiry on the Config delivery bucket.
- Terraform-managed log groups with 365-day retention for all platform
  Lambdas and auth-claims; existing groups adopted with import blocks.
- Test that every platform Lambda has a declared log group.
- docs/cost-management.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:08
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 15 to import, 2 to add, 17 to change, 2 to destroy.

Plan output
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
module.lambdas.aws_cloudwatch_log_group.lambda["app-secret-rollout"]: Preparing import... [id=/aws/lambda/javabin-app-secret-rollout]
module.lambdas.aws_cloudwatch_log_group.lambda["app-auth-sync"]: Preparing import... [id=/aws/lambda/javabin-app-auth-sync]
module.lambdas.aws_cloudwatch_log_group.lambda["password-set"]: Preparing import... [id=/aws/lambda/javabin-password-set]
module.lambdas.aws_cloudwatch_log_group.lambda["compliance-reporter"]: Preparing import... [id=/aws/lambda/javabin-compliance-reporter]
module.lambdas.aws_cloudwatch_log_group.lambda["slack-alert"]: Preparing import... [id=/aws/lambda/javabin-slack-alert]
module.lambdas.aws_cloudwatch_log_group.lambda["override-cleanup"]: Preparing import... [id=/aws/lambda/javabin-override-cleanup]
module.lambdas.aws_cloudwatch_log_group.lambda["plan-review"]: Preparing import... [id=/aws/lambda/javabin-plan-review]
module.lambdas.aws_cloudwatch_log_group.lambda["resource-tagger"]: Preparing import... [id=/aws/lambda/javabin-resource-tagger]
module.lambdas.aws_cloudwatch_log_group.lambda["apply-gate"]: Preparing import... [id=/aws/lambda/javabin-apply-gate]
module.lambdas.aws_cloudwatch_log_group.lambda["ci-broker"]: Preparing import... [id=/aws/lambda/javabin-ci-broker]
module.lambdas.aws_cloudwatch_log_group.lambda["daily-cost-check"]: Preparing import... [id=/aws/lambda/javabin-daily-cost-check]
module.lambdas.aws_cloudwatch_log_group.lambda["securityhub-summary"]: Preparing import... [id=/aws/lambda/javabin-securityhub-summary]
module.lambdas.aws_cloudwatch_log_group.lambda["cost-report"]: Preparing import... [id=/aws/lambda/javabin-cost-report]
module.lambdas.aws_cloudwatch_log_group.lambda["team-provisioner"]: Preparing import... [id=/aws/lambda/javabin-team-provisioner]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
module.identity.aws_cloudwatch_log_group.auth_claims: Preparing import... [id=/aws/lambda/javabin-auth-claims]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.cost_analytics.aws_budgets_budget.account will be updated in-place
  ~ resource "aws_budgets_budget" "account" {
        id                = "553637109631:javabin-account-monthly"
      ~ limit_amount      = "500.0" -> "600"
        name              = "javabin-account-monthly"
        tags              = {}
        # (8 unchanged attributes hidden)

      - notification {
          - comparison_operator        = "GREATER_THAN" -> null
          - notification_type          = "ACTUAL" -> null
          - subscriber_email_addresses = [] -> null
          - subscriber_sns_topic_arns  = [
              - "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ] -> null
          - threshold                  = 80 -> null
          - threshold_type             = "PERCENTAGE" -> null
        }
      + notification {
          + comparison_operator        = "GREATER_THAN"
          + notification_type          = "ACTUAL"
          + subscriber_email_addresses = []
          + subscriber_sns_topic_arns  = [
              + "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ]
          + threshold                  = 150
          + threshold_type             = "PERCENTAGE"
        }
      + notification {
          + comparison_operator        = "GREATER_THAN"
          + notification_type          = "FORECASTED"
          + subscriber_email_addresses = []
          + subscriber_sns_topic_arns  = [
              + "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ]
          + threshold                  = 100
          + threshold_type             = "PERCENTAGE"
        }

        # (2 unchanged blocks hidden)
    }

  # module.cost_analytics.aws_cloudwatch_metric_alarm.billing will be destroyed
  # (because aws_cloudwatch_metric_alarm.billing is not in configuration)
  - resource "aws_cloudwatch_metric_alarm" "billing" {
      - actions_enabled           = true -> null
      - alarm_actions             = [
          - "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm",
        ] -> null
      - alarm_description         = "Account estimated charges exceeded ${var.billing_alarm_threshold_usd}" -> null
      - alarm_name                = "javabin-billing-alarm" -> null
      - arn                       = "arn:aws:cloudwatch:us-east-1:553637109631:alarm:javabin-billing-alarm" -> null
      - comparison_operator       = "GreaterThanThreshold" -> null
      - datapoints_to_alarm       = 0 -> null
      - dimensions                = {
          - "Currency" = "USD"
        } -> null
      - evaluation_periods        = 1 -> null
      - id                        = "javabin-billing-alarm" -> null
      - insufficient_data_actions = [] -> null
      - metric_name               = "EstimatedCharges" -> null
      - namespace                 = "AWS/Billing" -> null
      - ok_actions                = [
          - "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm",
        ] -> null
      - period                    = 21600 -> null
      - statistic                 = "Maximum" -> null
      - tags                      = {} -> null
      - tags_all                  = {
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
      - threshold                 = 200 -> null
      - treat_missing_data        = "missing" -> null
    }

  # module.cost_analytics.aws_sns_topic.billing_alarm will be destroyed
  # (because aws_sns_topic.billing_alarm is not in configuration)
  - resource "aws_sns_topic" "billing_alarm" {
      - application_success_feedback_sample_rate = 0 -> null
      - arn                                      = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm" -> null
      - content_based_deduplication              = false -> null
      - fifo_topic                               = false -> null
      - firehose_success_feedback_sample_rate    = 0 -> null
      - http_success_feedback_sample_rate        = 0 -> null
      - id                                       = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm" -> null
      - lambda_success_feedback_sample_rate      = 0 -> null
      - name                                     = "javabin-billing-alarm" -> null
      - owner                                    = "553637109631" -> null
      - policy                                   = jsonencode(
            {
              - Id        = "__default_policy_ID"
              - Statement = [
                  - {
                      - Action    = [
                          - "SNS:GetTopicAttributes",
                          - "SNS:SetTopicAttributes",
                          - "SNS:AddPermission",
                          - "SNS:RemovePermission",
                          - "SNS:DeleteTopic",
                          - "SNS:Subscribe",
                          - "SNS:ListSubscriptionsByTopic",
                          - "SNS:Publish",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "AWS:SourceOwner" = "553637109631"
                            }
                        }
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "*"
                        }
                      - Resource  = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm"
                      - Sid       = "__default_statement_ID"
                    },
                ]
              - Version   = "2008-10-17"
            }
        ) -> null
      - signature_version                        = 0 -> null
      - sqs_success_feedback_sample_rate         = 0 -> null
      - tags                                     = {} -> null
      - tags_all                                 = {
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
    }

  # module.identity.aws_cloudwatch_log_group.auth_claims will be updated in-place
  # (imported from "/aws/lambda/javabin-auth-claims")
  ~ resource "aws_cloudwatch_log_group" "auth_claims" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-auth-claims"
        id                = "/aws/lambda/javabin-auth-claims"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-auth-claims"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["app-auth-sync"] will be updated in-place
  # (imported from "/aws/lambda/javabin-app-auth-sync")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-app-auth-sync"
        id                = "/aws/lambda/javabin-app-auth-sync"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-app-auth-sync"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["app-secret-rollout"] will be updated in-place
  # (imported from "/aws/lambda/javabin-app-secret-rollout")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-app-secret-rollout"
        id                = "/aws/lambda/javabin-app-secret-rollout"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-app-secret-rollout"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["apply-gate"] will be updated in-place
  # (imported from "/aws/lambda/javabin-apply-gate")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-apply-gate"
        id                = "/aws/lambda/javabin-apply-gate"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-apply-gate"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["budget-enforcer"] will be created
  + resource "aws_cloudwatch_log_group" "lambda" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + log_group_class   = (known after apply)
      + name              = "/aws/lambda/javabin-budget-enforcer"
      + name_prefix       = (known after apply)
      + retention_in_days = 365
      + skip_destroy      = false
      + tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["ci-broker"] will be updated in-place
  # (imported from "/aws/lambda/javabin-ci-broker")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-ci-broker"
        id                = "/aws/lambda/javabin-ci-broker"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-ci-broker"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["compliance-reporter"] will be updated in-place
  # (imported from "/aws/lambda/javabin-compliance-reporter")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-compliance-reporter"
        id                = "/aws/lambda/javabin-compliance-reporter"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-compliance-reporter"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["cost-report"] will be updated in-place
  # (imported from "/aws/lambda/javabin-cost-report")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-cost-report"
        id                = "/aws/lambda/javabin-cost-report"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-cost-report"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["daily-cost-check"] will be updated in-place
  # (imported from "/aws/lambda/javabin-daily-cost-check")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-daily-cost-check"
        id                = "/aws/lambda/javabin-daily-cost-check"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-daily-cost-check"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["override-cleanup"] will be updated in-place
  # (imported from "/aws/lambda/javabin-override-cleanup")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-override-cleanup"
        id                = "/aws/lambda/javabin-override-cleanup"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-override-cleanup"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["password-set"] will be updated in-place
  # (imported from "/aws/lambda/javabin-password-set")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-password-set"
        id                = "/aws/lambda/javabin-password-set"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-password-set"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["plan-review"] will be updated in-place
  # (imported from "/aws/lambda/javabin-plan-review")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-plan-review"
        id                = "/aws/lambda/javabin-plan-review"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-plan-review"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["resource-tagger"] will be updated in-place
  # (imported from "/aws/lambda/javabin-resource-tagger")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-resource-tagger"
        id                = "/aws/lambda/javabin-resource-tagger"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-resource-tagger"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["securityhub-summary"] will be updated in-place
  # (imported from "/aws/lambda/javabin-securityhub-summary")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-securityhub-summary"
        id                = "/aws/lambda/javabin-securityhub-summary"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-securityhub-summary"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["slack-alert"] will be updated in-place
  # (imported from "/aws/lambda/javabin-slack-alert")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-slack-alert"
        id                = "/aws/lambda/javabin-slack-alert"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-slack-alert"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["team-provisioner"] will be updated in-place
  # (imported from "/aws/lambda/javabin-team-provisioner")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-team-provisioner"
        id                = "/aws/lambda/javabin-team-provisioner"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-team-provisioner"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_config_configuration_recorder.main will be updated in-place
  ~ resource "aws_config_configuration_recorder" "main" {
        id       = "javabin-recorder"
        name     = "javabin-recorder"
        # (1 unchanged attribute hidden)

      ~ recording_group {
          ~ all_supported                 = true -> false
            # (2 unchanged attributes hidden)

          ~ exclusion_by_resource_types {
              ~ resource_types = [
                  + "AWS::EC2::NetworkInterface",
                ]
            }

          ~ recording_strategy {
              ~ use_only = "ALL_SUPPORTED_RESOURCE_TYPES" -> "EXCLUSION_BY_RESOURCE_TYPES"
            }
        }

      ~ recording_mode {
            # (1 unchanged attribute hidden)

          + recording_mode_override {
              + description         = "Fargate ENI churn re-records VPC and subnet relationships; daily is enough"
              + recording_frequency = "DAILY"
              + resource_types      = [
                  + "AWS::EC2::Subnet",
                  + "AWS::EC2::VPC",
                ]
            }
        }
    }

  # module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "config_logs" {
      + bucket                                 = "javabin-config-553637109631"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-config-history"
          + status = "Enabled"

          + expiration {
              + days                         = 730
              + expired_object_delete_marker = false
            }

          + filter {
            }
        }
    }

Plan: 15 to import, 2 to add, 17 to change, 2 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan imports 15 existing CloudWatch log groups, creates 2 resources, updates 17 (budget limit/notifications, Config recorder scope, log retention), and destroys a billing alarm and its SNS topic, with no database or security-group changes.

  • 💥 [destruction] Destroys module.cost_analytics aws_cloudwatch_metric_alarm.billing (javabin-billing-alarm) and aws_sns_topic.billing_alarm (javabin-billing-alarm). These are not in configuration, so this looks like an accidental removal of the account-level EstimatedCharges alarm. Its 200 USD threshold and SNS notification path would be lost. Confirm this is intended, or restore the resources in code, before applying.
  • 💰 [cost] Increases module.cost_analytics aws_budgets_budget.account limit from 500 to 600 USD. Notifications change from one ACTUAL alert at 80% to ACTUAL at 150% plus FORECASTED at 100%. The 80% early warning is removed and alerts fire later, which weakens spend control. Confirm the new limit is intended.
  • 🔒 [security] AWS Config recorder module.monitoring is changed from recording all supported resource types to EXCLUSION_BY_RESOURCE_TYPES, excluding AWS::EC2::NetworkInterface. Recording frequency for Subnet and VPC drops to DAILY. This reduces configuration history coverage, which can weaken compliance and audit evidence. Also, a new 730-day expiration lifecycle rule is added to the javabin-config-553637109631 bucket, so Config history older than two years will be deleted.
  • ✅ [routine] Imports 15 existing /aws/lambda log groups and one for auth-claims, and sets retention from never-expire (0) to 365 days. Tags are added. Expect some log history older than 365 days to be deleted over time. The new budget-enforcer log group is created with 365-day retention. Otherwise these are low-risk updates.

Excluding AWS::EC2::NetworkInterface would leave Security Hub EC2.180
(ENI source/destination check) without input and add a reason to
Config.1. Record ENI, VPC and Subnet with a DAILY override instead, and
document the trade-off, the realistic saving and the 7-day check.

Also widen the Lambda log-group guard test to every platform module,
and add the budget line to CLAUDE.md without rewriting the Cost Anomaly
line so it no longer conflicts with the ECS impaired-alert PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 15 to import, 2 to add, 17 to change, 2 to destroy.

Plan output
module.lambdas.aws_cloudwatch_log_group.lambda["daily-cost-check"]: Preparing import... [id=/aws/lambda/javabin-daily-cost-check]
module.lambdas.aws_cloudwatch_log_group.lambda["override-cleanup"]: Preparing import... [id=/aws/lambda/javabin-override-cleanup]
module.lambdas.aws_cloudwatch_log_group.lambda["cost-report"]: Preparing import... [id=/aws/lambda/javabin-cost-report]
module.lambdas.aws_cloudwatch_log_group.lambda["plan-review"]: Preparing import... [id=/aws/lambda/javabin-plan-review]
module.lambdas.aws_cloudwatch_log_group.lambda["resource-tagger"]: Preparing import... [id=/aws/lambda/javabin-resource-tagger]
module.lambdas.aws_cloudwatch_log_group.lambda["password-set"]: Preparing import... [id=/aws/lambda/javabin-password-set]
module.lambdas.aws_cloudwatch_log_group.lambda["slack-alert"]: Preparing import... [id=/aws/lambda/javabin-slack-alert]
module.lambdas.aws_cloudwatch_log_group.lambda["securityhub-summary"]: Preparing import... [id=/aws/lambda/javabin-securityhub-summary]
module.lambdas.aws_cloudwatch_log_group.lambda["ci-broker"]: Preparing import... [id=/aws/lambda/javabin-ci-broker]
module.lambdas.aws_cloudwatch_log_group.lambda["apply-gate"]: Preparing import... [id=/aws/lambda/javabin-apply-gate]
module.lambdas.aws_cloudwatch_log_group.lambda["app-auth-sync"]: Preparing import... [id=/aws/lambda/javabin-app-auth-sync]
module.lambdas.aws_cloudwatch_log_group.lambda["app-secret-rollout"]: Preparing import... [id=/aws/lambda/javabin-app-secret-rollout]
module.lambdas.aws_cloudwatch_log_group.lambda["team-provisioner"]: Preparing import... [id=/aws/lambda/javabin-team-provisioner]
module.lambdas.aws_cloudwatch_log_group.lambda["compliance-reporter"]: Preparing import... [id=/aws/lambda/javabin-compliance-reporter]
module.identity.aws_cloudwatch_log_group.auth_claims: Preparing import... [id=/aws/lambda/javabin-auth-claims]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.cost_analytics.aws_budgets_budget.account will be updated in-place
  ~ resource "aws_budgets_budget" "account" {
        id                = "553637109631:javabin-account-monthly"
      ~ limit_amount      = "500.0" -> "600"
        name              = "javabin-account-monthly"
        tags              = {}
        # (8 unchanged attributes hidden)

      - notification {
          - comparison_operator        = "GREATER_THAN" -> null
          - notification_type          = "ACTUAL" -> null
          - subscriber_email_addresses = [] -> null
          - subscriber_sns_topic_arns  = [
              - "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ] -> null
          - threshold                  = 80 -> null
          - threshold_type             = "PERCENTAGE" -> null
        }
      + notification {
          + comparison_operator        = "GREATER_THAN"
          + notification_type          = "ACTUAL"
          + subscriber_email_addresses = []
          + subscriber_sns_topic_arns  = [
              + "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ]
          + threshold                  = 150
          + threshold_type             = "PERCENTAGE"
        }
      + notification {
          + comparison_operator        = "GREATER_THAN"
          + notification_type          = "FORECASTED"
          + subscriber_email_addresses = []
          + subscriber_sns_topic_arns  = [
              + "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
            ]
          + threshold                  = 100
          + threshold_type             = "PERCENTAGE"
        }

        # (2 unchanged blocks hidden)
    }

  # module.cost_analytics.aws_cloudwatch_metric_alarm.billing will be destroyed
  # (because aws_cloudwatch_metric_alarm.billing is not in configuration)
  - resource "aws_cloudwatch_metric_alarm" "billing" {
      - actions_enabled           = true -> null
      - alarm_actions             = [
          - "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm",
        ] -> null
      - alarm_description         = "Account estimated charges exceeded ${var.billing_alarm_threshold_usd}" -> null
      - alarm_name                = "javabin-billing-alarm" -> null
      - arn                       = "arn:aws:cloudwatch:us-east-1:553637109631:alarm:javabin-billing-alarm" -> null
      - comparison_operator       = "GreaterThanThreshold" -> null
      - datapoints_to_alarm       = 0 -> null
      - dimensions                = {
          - "Currency" = "USD"
        } -> null
      - evaluation_periods        = 1 -> null
      - id                        = "javabin-billing-alarm" -> null
      - insufficient_data_actions = [] -> null
      - metric_name               = "EstimatedCharges" -> null
      - namespace                 = "AWS/Billing" -> null
      - ok_actions                = [
          - "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm",
        ] -> null
      - period                    = 21600 -> null
      - statistic                 = "Maximum" -> null
      - tags                      = {} -> null
      - tags_all                  = {
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
      - threshold                 = 200 -> null
      - treat_missing_data        = "missing" -> null
    }

  # module.cost_analytics.aws_sns_topic.billing_alarm will be destroyed
  # (because aws_sns_topic.billing_alarm is not in configuration)
  - resource "aws_sns_topic" "billing_alarm" {
      - application_success_feedback_sample_rate = 0 -> null
      - arn                                      = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm" -> null
      - content_based_deduplication              = false -> null
      - fifo_topic                               = false -> null
      - firehose_success_feedback_sample_rate    = 0 -> null
      - http_success_feedback_sample_rate        = 0 -> null
      - id                                       = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm" -> null
      - lambda_success_feedback_sample_rate      = 0 -> null
      - name                                     = "javabin-billing-alarm" -> null
      - owner                                    = "553637109631" -> null
      - policy                                   = jsonencode(
            {
              - Id        = "__default_policy_ID"
              - Statement = [
                  - {
                      - Action    = [
                          - "SNS:GetTopicAttributes",
                          - "SNS:SetTopicAttributes",
                          - "SNS:AddPermission",
                          - "SNS:RemovePermission",
                          - "SNS:DeleteTopic",
                          - "SNS:Subscribe",
                          - "SNS:ListSubscriptionsByTopic",
                          - "SNS:Publish",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "AWS:SourceOwner" = "553637109631"
                            }
                        }
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "*"
                        }
                      - Resource  = "arn:aws:sns:us-east-1:553637109631:javabin-billing-alarm"
                      - Sid       = "__default_statement_ID"
                    },
                ]
              - Version   = "2008-10-17"
            }
        ) -> null
      - signature_version                        = 0 -> null
      - sqs_success_feedback_sample_rate         = 0 -> null
      - tags                                     = {} -> null
      - tags_all                                 = {
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
    }

  # module.identity.aws_cloudwatch_log_group.auth_claims will be updated in-place
  # (imported from "/aws/lambda/javabin-auth-claims")
  ~ resource "aws_cloudwatch_log_group" "auth_claims" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-auth-claims"
        id                = "/aws/lambda/javabin-auth-claims"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-auth-claims"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["app-auth-sync"] will be updated in-place
  # (imported from "/aws/lambda/javabin-app-auth-sync")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-app-auth-sync"
        id                = "/aws/lambda/javabin-app-auth-sync"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-app-auth-sync"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["app-secret-rollout"] will be updated in-place
  # (imported from "/aws/lambda/javabin-app-secret-rollout")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-app-secret-rollout"
        id                = "/aws/lambda/javabin-app-secret-rollout"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-app-secret-rollout"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["apply-gate"] will be updated in-place
  # (imported from "/aws/lambda/javabin-apply-gate")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-apply-gate"
        id                = "/aws/lambda/javabin-apply-gate"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-apply-gate"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["budget-enforcer"] will be created
  + resource "aws_cloudwatch_log_group" "lambda" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + log_group_class   = (known after apply)
      + name              = "/aws/lambda/javabin-budget-enforcer"
      + name_prefix       = (known after apply)
      + retention_in_days = 365
      + skip_destroy      = false
      + tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["ci-broker"] will be updated in-place
  # (imported from "/aws/lambda/javabin-ci-broker")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-ci-broker"
        id                = "/aws/lambda/javabin-ci-broker"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-ci-broker"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["compliance-reporter"] will be updated in-place
  # (imported from "/aws/lambda/javabin-compliance-reporter")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-compliance-reporter"
        id                = "/aws/lambda/javabin-compliance-reporter"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-compliance-reporter"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["cost-report"] will be updated in-place
  # (imported from "/aws/lambda/javabin-cost-report")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-cost-report"
        id                = "/aws/lambda/javabin-cost-report"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-cost-report"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["daily-cost-check"] will be updated in-place
  # (imported from "/aws/lambda/javabin-daily-cost-check")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-daily-cost-check"
        id                = "/aws/lambda/javabin-daily-cost-check"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-daily-cost-check"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["override-cleanup"] will be updated in-place
  # (imported from "/aws/lambda/javabin-override-cleanup")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-override-cleanup"
        id                = "/aws/lambda/javabin-override-cleanup"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-override-cleanup"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["password-set"] will be updated in-place
  # (imported from "/aws/lambda/javabin-password-set")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-password-set"
        id                = "/aws/lambda/javabin-password-set"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-password-set"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["plan-review"] will be updated in-place
  # (imported from "/aws/lambda/javabin-plan-review")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-plan-review"
        id                = "/aws/lambda/javabin-plan-review"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-plan-review"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["resource-tagger"] will be updated in-place
  # (imported from "/aws/lambda/javabin-resource-tagger")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-resource-tagger"
        id                = "/aws/lambda/javabin-resource-tagger"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-resource-tagger"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["securityhub-summary"] will be updated in-place
  # (imported from "/aws/lambda/javabin-securityhub-summary")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-securityhub-summary"
        id                = "/aws/lambda/javabin-securityhub-summary"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-securityhub-summary"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["slack-alert"] will be updated in-place
  # (imported from "/aws/lambda/javabin-slack-alert")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-slack-alert"
        id                = "/aws/lambda/javabin-slack-alert"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-slack-alert"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["team-provisioner"] will be updated in-place
  # (imported from "/aws/lambda/javabin-team-provisioner")
  ~ resource "aws_cloudwatch_log_group" "lambda" {
        arn               = "arn:aws:logs:eu-central-1:553637109631:log-group:/aws/lambda/javabin-team-provisioner"
        id                = "/aws/lambda/javabin-team-provisioner"
        log_group_class   = "STANDARD"
        name              = "/aws/lambda/javabin-team-provisioner"
      ~ retention_in_days = 0 -> 365
        skip_destroy      = false
        tags              = {}
      ~ tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_config_configuration_recorder.main will be updated in-place
  ~ resource "aws_config_configuration_recorder" "main" {
        id       = "javabin-recorder"
        name     = "javabin-recorder"
        # (1 unchanged attribute hidden)

      ~ recording_mode {
            # (1 unchanged attribute hidden)

          + recording_mode_override {
              + description         = "Fargate ENI churn: daily is enough for ENIs and the VPC/subnet relationships they re-record"
              + recording_frequency = "DAILY"
              + resource_types      = [
                  + "AWS::EC2::NetworkInterface",
                  + "AWS::EC2::Subnet",
                  + "AWS::EC2::VPC",
                ]
            }
        }

        # (1 unchanged block hidden)
    }

  # module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "config_logs" {
      + bucket                                 = "javabin-config-553637109631"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-config-history"
          + status = "Enabled"

          + expiration {
              + days                         = 730
              + expired_object_delete_marker = false
            }

          + filter {
            }
        }
    }

Plan: 15 to import, 2 to add, 17 to change, 2 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan imports 15 existing CloudWatch log groups and sets 365-day retention on them, creates 2 resources, updates 17 (budget limit/notifications, Config recorder frequency, log retention), and destroys the billing alarm and its SNS topic in us-east-1; no database, security group, or IAM changes, and nothing is internet-exposed.

  • 💥 [destruction] aws_cloudwatch_metric_alarm.billing and aws_sns_topic.billing_alarm (us-east-1, javabin-billing-alarm) are removed from configuration and will be destroyed. This removes the account-level EstimatedCharges alarm and its notification topic, so the account loses its billing alarm (the separate budget notifications may partly cover this). Confirm this is intended, or that the replacement is in place before applying.
  • 💰 [cost] Account budget limit rises from 500 to 600 USD. The existing 80% ACTUAL alert is replaced with notifications at 150% ACTUAL and 100% FORECASTED, all sent to the javabin-alerts SNS topic. The 80% early warning is dropped, so spend could reach the limit with less lead time. Confirm the change in limit and thresholds is intended.
  • ✅ [routine] 15 existing log groups (Lambda and auth-claims) are imported and retention changes from never-expire (0) to 365 days. Log data older than a year will be deleted, and the tags are added. This is a standard governance change, but it is irreversible for logs past 365 days.
  • ✅ [routine] AWS Config recorder gets a recording_mode_override so NetworkInterface, Subnet and VPC are recorded daily instead of continuously. Lower recording cost, with delayed visibility of network changes for compliance tooling. Also adds a 730-day expiration lifecycle rule to the javabin-config-553637109631 bucket, which deletes Config history older than two years.
  • ✅ [routine] New log group budget-enforcer is created with 365-day retention. Low cost, no security impact. Tag changes only on the remaining resources. No IAM, security group, or public-access changes are present.

This branch has not been deployed

No deployments
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.

1 participant