Skip to content

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

Merged
Alexanderamiri merged 1 commit into
mainfrom
feat/cost-guardrails
Oct 26, 2026
Merged

Alexanderamiri merged 1 commit 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

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

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

… control

Budgets, Cost Anomaly Detection, Config recording ENI/VPC/Subnet daily
(not excluded, so Security Hub EC2.180 and Config.1 keep working), and
bounded retention for every platform Lambda log group (existing groups
adopted with import blocks).

Rebased onto #163 (which already expires Config history after two years,
so this PR's duplicate lifecycle rule is dropped) and #158 (adds a log
group for the new javabin-auth-claims-external trigger).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
module.identity.aws_cloudwatch_log_group.auth_claims: Preparing import... [id=/aws/lambda/javabin-auth-claims]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
module.lambdas.aws_cloudwatch_log_group.lambda["override-cleanup"]: Preparing import... [id=/aws/lambda/javabin-override-cleanup]
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["cost-report"]: Preparing import... [id=/aws/lambda/javabin-cost-report]
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["password-set"]: Preparing import... [id=/aws/lambda/javabin-password-set]
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["daily-cost-check"]: Preparing import... [id=/aws/lambda/javabin-daily-cost-check]
module.lambdas.aws_cloudwatch_log_group.lambda["resource-tagger"]: Preparing import... [id=/aws/lambda/javabin-resource-tagger]
module.lambdas.aws_cloudwatch_log_group.lambda["compliance-reporter"]: Preparing import... [id=/aws/lambda/javabin-compliance-reporter]
module.lambdas.aws_cloudwatch_log_group.lambda["plan-review"]: Preparing import... [id=/aws/lambda/javabin-plan-review]
module.lambdas.aws_cloudwatch_log_group.lambda["apply-gate"]: Preparing import... [id=/aws/lambda/javabin-apply-gate]
module.lambdas.aws_cloudwatch_log_group.lambda["team-provisioner"]: Preparing import... [id=/aws/lambda/javabin-team-provisioner]
module.lambdas.aws_cloudwatch_log_group.lambda["slack-alert"]: Preparing import... [id=/aws/lambda/javabin-slack-alert]

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.identity.aws_cloudwatch_log_group.auth_claims_external will be created
  + resource "aws_cloudwatch_log_group" "auth_claims_external" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + log_group_class   = (known after apply)
      + name              = "/aws/lambda/javabin-auth-claims-external"
      + 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.identity.aws_cognito_identity_provider.internal_google[0] will be updated in-place
  ~ resource "aws_cognito_identity_provider" "internal_google" {
        id                = "eu-central-1_Icikv3dtD:Google"
      ~ provider_details  = {
          ~ "authorize_url"                 = "https://accounts.google.com/o/oauth2/v2/auth" -> "https://accounts.google.com/o/oauth2/v2/auth?hd=java.no&prompt=select_account"
            # (8 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)
    }

  # 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)
    }

Plan: 15 to import, 2 to add, 18 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 makes 18 in-place updates, creates 2 new resources, and destroys the billing alarm and its SNS topic; nothing here opens security groups or changes IAM, but the billing alarm removal and budget changes warrant a look before apply.

  • 💥 [destruction] The us-east-1 billing alarm (javabin-billing-alarm, AWS/Billing EstimatedCharges > $200) and its SNS topic javabin-billing-alarm are being destroyed because they are no longer in configuration. This removes a cost safety net, and the AWS/Billing metric is only published in us-east-1, so the alarm must be preserved or replaced elsewhere. Confirm this is intentional, since the budget notifications may be meant to replace it.
  • 💰 [cost] The account monthly budget limit rises from 500 to 600 USD, and notification thresholds are rewritten: the ACTUAL 80% alert is replaced by ACTUAL 150% and a new FORECASTED 100% alert is added. The new ACTUAL 150% threshold means the first actual-spend warning arrives only after spend hits 150% of budget, so overspend warnings come much later than before.
  • ✅ [routine] Fifteen existing CloudWatch log groups (Lambda and auth_claims) are imported into state, and 14 of them are updated to set retention from never-expire (0) to 365 days. This is a sensible retention change, but logs older than a year will be deleted on the next retention pass.
  • ✅ [routine] Two new log groups are created (javabin-auth-claims-external and javabin-budget-enforcer), each with 365-day retention. Their cost is minimal, and no new compute or network resources are added.
  • 🔒 [security] The Cognito Google identity provider's authorize_url gains hd=java.no&prompt=select_account. This restricts Google sign-in to java.no Workspace accounts through the hosted domain hint. The hd parameter is only a UI hint and is not enforced server-side, so verify that the app also validates the hd claim in the ID token if this is meant as an access control. The AWS Config recorder also gains a daily recording override for ENI, subnet and VPC resources, which reduces recording fidelity for those types but is low risk.

@Alexanderamiri
Alexanderamiri merged commit 8b6ab4c into main Oct 26, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the feat/cost-guardrails branch October 26, 2026 14:12
Alexanderamiri added a commit that referenced this pull request Oct 26, 2026
## Why

`testteam-platform-test-app` has been crash-looping for weeks with
desired=1 and running=0, because of a `CannotPullContainerError` (its
ECR repo is empty). That is about 80 failed task starts a day, and
nobody noticed. Each attempt creates and deletes a Fargate ENI, which
AWS Config records. This was the main driver of about $29/mo in Config
charges found in the 2026-10 cost audit. PR #159 reduces the Config
side. This PR makes the crash loop itself visible.

## What

- `monitoring/main.tf`: new EventBridge rule
`javabin-ecs-task-start-impaired`. It matches `source=aws.ecs`,
`detail-type=ECS Service Action` and
`eventName=SERVICE_TASK_START_IMPAIRED`, for the `javabin-platform`
cluster only (legacy CDK clusters are excluded). The target is the
`javabin-security` SNS topic, whose policy already allows
`events.amazonaws.com`. slack-alert sends non-finding events from that
topic to #javabin-infra-alerts. I chose that channel over the cost
channel because this is an operational problem.
- `slack_alert/handler.py`:
- New `format_ecs_service_impaired` formatter, routed before the generic
EventBridge branch. Without it, the event would fall through to the
IAM-change formatter.
- The message shows the service, the cluster and a link to the service's
events page in the ECS console.
- Dedup uses the existing persistent `javabin-alert-dedup` table, keyed
per service ARN per UTC day. ECS keeps emitting the event while the
service is impaired.
- New test `terraform/lambda-src/tests/test_slack_alert_ecs.py` checks
the formatting, once-per-day dedup per service, and that other ECS
service actions are ignored.
- Docs: added a line to the CLAUDE.md alert routing section and to
`docs/lambda-functions.md`.

## Expected plan

2 to add: `aws_cloudwatch_event_rule.ecs_task_start_impaired` and
`aws_cloudwatch_event_target.ecs_task_start_impaired_sns`. 1 in-place
update: the slack-alert Lambda code. No IAM changes, because slack-alert
already has GetItem/PutItem on the dedup table.

## How verified

- `python3 -m unittest discover -s terraform/lambda-src/tests` (67 OK, 3
new) and `-s scripts/tests` pass.
- `terraform fmt -recursive -check`, then `terraform init
-backend=false` and `terraform validate` pass.

## Follow-ups

- Once this is applied, the first alert should be for
`testteam-platform-test-app`. Scale it to 0 or push its image.
- It is independent of #159. Both touch `monitoring/main.tf`, but in
different blocks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Alexanderamiri added a commit that referenced this pull request Oct 26, 2026
app.yaml calls/provides declare private app-to-app edges, optionally with
OAuth scopes. Every app gets its own task security group and a Cloud Map
name in the new javabin.internal namespace; app-peers-sync opens ingress
only where both sides agree and manages the M2M resource servers and
clients. The developer boundary protects the shared namespace and the
private zone's records (6,067 of 6,144 bytes).

Rebased onto routing.login (#158), RDS/DNS adoption (#155), Lambda log
retention (#159, adds the app-peers-sync log group) and the app-auth-sync
restart rules (#167).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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