Skip to content

Tags as primitive, alert enrichment, budget enforcement, RDS module#77

Merged
Alexanderamiri merged 2 commits into
mainfrom
feat/tags-alerts-rds-budget
Mar 17, 2026
Merged

Tags as primitive, alert enrichment, budget enforcement, RDS module#77
Alexanderamiri merged 2 commits into
mainfrom
feat/tags-alerts-rds-budget

Conversation

@Alexanderamiri
Copy link
Copy Markdown
Member

Summary

Foundation changes to make tags a first-class primitive for ABAC, cost attribution, and auditability. Plus alert enrichment, budget enforcement, and RDS support.

Tags as primitive

  • Tag schema: 5 static tags (team, service, repo, environment, managed-by) via provider default_tags. Dropped project tag (always "javabin", zero information).
  • Resource tagger Lambda: EventBridge-triggered (wildcard {"prefix": "aws."} match), auto-tags created-by + commit from CloudTrail session names. Tags added via AWS API — invisible to Terraform, no drift.
  • Cost allocation tags: Activated for all 7 tag keys so Cost Explorer can group by team/service.
  • ECS tag propagation: propagate_tags = SERVICE so Fargate task costs are attributed to teams.

Alert enrichment (Task A)

  • CI session names changed to {actor}-{sha8}-{run_id} in all 4 workflows.
  • slack_alert parse_identity() extracts actor/commit from new format.
  • Cost reports (daily + weekly) include per-team tag breakdown.

Budget enforcement (Task D)

  • New budget-enforcer Lambda: scales ECS services to desired_count=0 at 200% budget.
  • team_provisioner adds 200% notification alongside existing 80%.

RDS module (Task E)

  • New service-rds module: PostgreSQL with Secrets Manager password, private subnets, ECS SG ingress.
  • registry.py + expand-modules.py updated with engine-based routing (postgres vs dynamodb).

IAM restructure

  • Team deny policy: ABAC (ARN-scoped) where AWS supports tags (SNS, S3, ECS, ELB). Explicit denies only where AWS lacks tag conditions (EC2 VPC, GuardDuty, SecurityHub, Config, CloudTrail, Organizations, IAM).
  • service-role module: configurable trusted_services (ECS/EC2/Lambda).
  • EventBridge resource-tagger uses wildcard; monitoring rules keep curated lists (documented volume rationale).

Test plan

  • terraform plan shows tag migration (project removed, service+repo added) on existing resources
  • After apply: resources show 5 Terraform-managed tags in console
  • Trigger CI run → Slack alert shows actor name + commit link
  • Next day: Cost Explorer GroupBy team returns per-team costs
  • Create test resource → resource-tagger tags it within 15 min
  • Invoke budget-enforcer with test payload → ECS service scales to 0
  • Test app with engine: postgres in app.yaml → RDS created in private subnet

)

Foundation changes:
- Tag schema: drop project tag, add repo tag. 5 static tags (team, service,
  repo, environment, managed-by) applied via provider default_tags.
- Resource tagger Lambda: EventBridge-triggered (wildcard prefix match),
  auto-tags created-by + commit from CloudTrail session names. Tags added
  via AWS API outside Terraform — no drift or plan noise.
- Cost allocation tags activated for all 7 tag keys.
- ECS tag propagation: enable_ecs_managed_tags + propagate_tags = SERVICE
  so Fargate task-level compute costs are attributed to teams.

Alert enrichment:
- CI session names changed to {actor}-{sha8}-{run_id} in all 4 workflows.
- slack_alert parse_identity() updated to extract actor/commit from new format.
- Cost reports (daily + weekly) now include per-team tag breakdown.

Budget enforcement:
- New budget_enforcer Lambda: scales ECS services to desired_count=0 when
  team exceeds 200% of budget. Triggered via SNS from AWS Budgets.
- team_provisioner sync_budget adds 200% notification alongside existing 80%.

RDS module:
- New service-rds module: PostgreSQL with Secrets Manager password,
  private subnet placement, security group scoped to ECS tasks SG.
- Registry + expand-modules updated with engine-based routing (postgres vs dynamodb).

IAM restructure:
- Team deny policy: ABAC (ARN-scoped) where AWS supports tags (SNS, S3,
  ECS, ELB). Explicit denies only where AWS lacks tag conditions (EC2 VPC,
  GuardDuty, SecurityHub, Config, CloudTrail, Organizations, IAM users).
- Service-role module: configurable trusted_services (ECS/EC2/Lambda).
- EventBridge monitoring rules: documented volume rationale for curated lists.
- CLAUDE.md: add budget-enforcer, resource-tagger, ci-broker to Lambda
  table and alert routing diagram. Update function count to 11.
- lambda-functions.md: add budget-enforcer and resource-tagger sections,
  update team-provisioner from stub to working status.
- reusable-modules.md: add service-rds module, document trusted_services
  on service-role, add ECS tag propagation note.
- platform-modules.md: update Lambda count, add cost allocation tags.
@github-actions
Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 22 to add, 81 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...

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

Terraform will perform the following actions:

  # module.compute.aws_ecr_repository.ci["jvm"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-jvm"
        name                 = "javabin-ci-jvm"
      ~ tags                 = {
            "Name"    = "javabin-ci-jvm"
          - "project" = "javabin" -> null
        }
      ~ tags_all             = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.compute.aws_ecr_repository.ci["platform"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-platform"
        name                 = "javabin-ci-platform"
      ~ tags                 = {
            "Name"    = "javabin-ci-platform"
          - "project" = "javabin" -> null
        }
      ~ tags_all             = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.compute.aws_ecr_repository.ci["ts"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-ts"
        name                 = "javabin-ci-ts"
      ~ tags                 = {
            "Name"    = "javabin-ci-ts"
          - "project" = "javabin" -> null
        }
      ~ tags_all             = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.compute.aws_ecs_cluster.main will be updated in-place
  ~ resource "aws_ecs_cluster" "main" {
        id       = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
        name     = "javabin-platform"
      ~ tags     = {
            "Name"    = "javabin-platform"
          - "project" = "javabin" -> null
        }
      ~ tags_all = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (1 unchanged attribute hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ tags             = {
            "Name"    = "javabin-developer-boundary"
          - "project" = "javabin" -> null
        }
      ~ tags_all         = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_role.ci_app_broker will be updated in-place
  ~ resource "aws_iam_role" "ci_app_broker" {
        id                    = "javabin-ci-app-broker"
        name                  = "javabin-ci-app-broker"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_apply_gate will be updated in-place
  ~ resource "aws_iam_role" "ci_apply_gate" {
        id                    = "javabin-ci-apply-gate"
        name                  = "javabin-ci-apply-gate"
      ~ tags                  = {
            "Name"    = "javabin-ci-apply-gate"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_deploy["platform-test-team"] will be updated in-place
  ~ resource "aws_iam_role" "ci_deploy" {
        id                    = "javabin-ci-deploy-platform-test-team"
        name                  = "javabin-ci-deploy-platform-test-team"
      ~ tags                  = {
            "Name"    = "javabin-ci-deploy-platform-test-team"
          - "project" = "javabin" -> null
            "team"    = "platform-test-team"
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra will be updated in-place
  ~ resource "aws_iam_role" "ci_infra" {
        id                    = "javabin-ci-infra"
        name                  = "javabin-ci-infra"
      ~ tags                  = {
            "Name"    = "javabin-ci-infra"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra_plan will be updated in-place
  ~ resource "aws_iam_role" "ci_infra_plan" {
        id                    = "javabin-ci-infra-plan"
        name                  = "javabin-ci-infra-plan"
      ~ tags                  = {
            "Name"    = "javabin-ci-infra-plan"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_override_approver will be updated in-place
  ~ resource "aws_iam_role" "ci_override_approver" {
        id                    = "javabin-ci-override-approver"
        name                  = "javabin-ci-override-approver"
      ~ tags                  = {
            "Name"    = "javabin-ci-override-approver"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_registry will be updated in-place
  ~ resource "aws_iam_role" "ci_registry" {
        id                    = "javabin-ci-registry"
        name                  = "javabin-ci-registry"
      ~ tags                  = {
            "Name"    = "javabin-ci-registry"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_team["platform-test-team"] will be updated in-place
  ~ resource "aws_iam_role" "ci_team" {
        id                    = "javabin-ci-team-platform-test-team"
        name                  = "javabin-ci-team-platform-test-team"
      ~ tags                  = {
            "Name"    = "javabin-ci-team-platform-test-team"
          - "project" = "javabin" -> null
            "team"    = "platform-test-team"
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ecs_execution will be updated in-place
  ~ resource "aws_iam_role" "ecs_execution" {
        id                    = "javabin-ecs-execution"
        name                  = "javabin-ecs-execution"
      ~ tags                  = {
            "Name"    = "javabin-ecs-execution"
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role_policy.ci_team_deny["platform-test-team"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_deny" {
        id     = "javabin-ci-team-platform-test-team:deny-platform-operations"
        name   = "deny-platform-operations"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action   = [
                            # (12 unchanged elements hidden)
                            "ec2:DeleteRouteTable",
                          - "ec2:CreateSecurityGroup",
                          - "ec2:DeleteSecurityGroup",
                          - "ec2:AuthorizeSecurityGroupIngress",
                          - "ec2:RevokeSecurityGroupIngress",
                          - "ec2:AuthorizeSecurityGroupEgress",
                          - "ec2:RevokeSecurityGroupEgress",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  - {
                      - Action   = [
                          - "elasticloadbalancingv2:CreateLoadBalancer",
                          - "elasticloadbalancingv2:DeleteLoadBalancer",
                          - "ecs:CreateCluster",
                          - "ecs:DeleteCluster",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyLoadBalancerAndCluster"
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecurityServices"
                    },
                    # (1 unchanged element hidden)
                    {
                        Action   = [
                            "iam:CreateUser",
                            "iam:CreateAccessKey",
                            "iam:CreateLoginProfile",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyDangerousIAM"
                    },
                  ~ {
                      ~ Action   = [
                          - "sns:CreateTopic",
                            "sns:DeleteTopic",
                          + "sns:SetTopicAttributes",
                          + "sns:Subscribe",
                          + "sns:Unsubscribe",
                        ]
                      ~ Resource = "*" -> [
                          + "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
                          + "arn:aws:sns:eu-central-1:553637109631:javabin-security",
                          + "arn:aws:sns:eu-central-1:553637109631:javabin-budget-enforcement",
                        ]
                        # (2 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action   = "s3:DeleteBucket" -> [
                          + "s3:DeleteBucket",
                          + "s3:PutBucketPolicy",
                          + "s3:DeleteBucketPolicy",
                        ]
                      ~ Resource = "*" -> [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                        ]
                      ~ Sid      = "DenyStateBucketDeletion" -> "DenyPlatformS3"
                        # (1 unchanged attribute hidden)
                    },
                  + {
                      + Action   = [
                          + "ecs:DeleteCluster",
                          + "ecs:UpdateCluster",
                          + "elasticloadbalancingv2:DeleteLoadBalancer",
                          + "elasticloadbalancingv2:ModifyLoadBalancerAttributes",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancingv2:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                        ]
                      + Sid      = "DenyPlatformCompute"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.identity.aws_cognito_user_pool.external will be updated in-place
  ~ resource "aws_cognito_user_pool" "external" {
        id                        = "eu-central-1_gdFOsE4EM"
        name                      = "javabin-external"
      ~ tags                      = {
            "Name"    = "javabin-external"
          - "project" = "javabin" -> null
        }
      ~ tags_all                  = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (10 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.identity.aws_cognito_user_pool.internal will be updated in-place
  ~ resource "aws_cognito_user_pool" "internal" {
        id                        = "eu-central-1_Icikv3dtD"
        name                      = "javabin-internal"
      ~ tags                      = {
            "Name"    = "javabin-internal"
          - "project" = "javabin" -> null
        }
      ~ tags_all                  = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (11 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.ingress.aws_acm_certificate.wildcard will be updated in-place
  ~ resource "aws_acm_certificate" "wildcard" {
        id                        = "arn:aws:acm:eu-central-1:553637109631:certificate/9b79f56a-3719-4c62-8970-6f08985a7e5b"
      ~ tags                      = {
            "Name"    = "javazone.no-wildcard"
          - "project" = "javabin" -> null
        }
      ~ tags_all                  = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (14 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.ingress.aws_lb.main will be updated in-place
  ~ resource "aws_lb" "main" {
        id                                          = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-platform-alb/bec1dd43ab8341b9"
        name                                        = "javabin-platform-alb"
      ~ tags                                        = {
            "Name"    = "javabin-platform-alb"
          - "project" = "javabin" -> null
        }
      ~ tags_all                                    = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (4 unchanged elements hidden)
        }
        # (23 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.ingress.aws_lb_listener.http_redirect will be updated in-place
  ~ resource "aws_lb_listener" "http_redirect" {
        id                                   = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:listener/app/javabin-platform-alb/bec1dd43ab8341b9/1d92e19ae75aa59b"
      ~ tags                                 = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                             = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.ingress.aws_lb_listener.https will be updated in-place
  ~ resource "aws_lb_listener" "https" {
        id                                   = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:listener/app/javabin-platform-alb/bec1dd43ab8341b9/500c9c2b4186bf45"
      ~ tags                                 = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                             = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.compliance_reporter_trigger will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "compliance_reporter_trigger" {
        id             = "javabin-compliance-reporter-trigger"
        name           = "javabin-compliance-reporter-trigger"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.cost_report_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "cost_report_schedule" {
        id                  = "javabin-cost-report-schedule"
        name                = "javabin-cost-report-schedule"
      ~ tags                = {
          - "project" = "javabin" -> null
        }
      ~ tags_all            = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.daily_cost_check_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "daily_cost_check_schedule" {
        id                  = "javabin-daily-cost-check-schedule"
        name                = "javabin-daily-cost-check-schedule"
      ~ tags                = {
          - "project" = "javabin" -> null
        }
      ~ tags_all            = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.override_cleanup_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "override_cleanup_schedule" {
        id                  = "javabin-override-cleanup-schedule"
        name                = "javabin-override-cleanup-schedule"
      ~ tags                = {
          - "project" = "javabin" -> null
        }
      ~ tags_all            = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.resource_tagger_trigger will be created
  + resource "aws_cloudwatch_event_rule" "resource_tagger_trigger" {
      + arn            = (known after apply)
      + description    = "Tag newly created resources with creator attribution"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + eventName = [
                      + {
                          + prefix = "Create"
                        },
                      + {
                          + prefix = "Run"
                        },
                    ]
                }
              + detail-type = [
                  + "AWS API Call via CloudTrail",
                ]
              + source      = [
                  + {
                      + prefix = "aws."
                    },
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-resource-tagger-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.securityhub_summary_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "securityhub_summary_schedule" {
        id                  = "javabin-securityhub-summary-schedule"
        name                = "javabin-securityhub-summary-schedule"
      ~ tags                = {
          - "project" = "javabin" -> null
        }
      ~ tags_all            = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_target.resource_tagger will be created
  + resource "aws_cloudwatch_event_target" "resource_tagger" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-resource-tagger-trigger"
      + target_id      = "invoke-resource-tagger"
    }

  # module.lambdas.aws_iam_role.apply_gate will be updated in-place
  ~ resource "aws_iam_role" "apply_gate" {
        id                    = "javabin-apply-gate"
        name                  = "javabin-apply-gate"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.budget_enforcer will be created
  + resource "aws_iam_role" "budget_enforcer" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-budget-enforcer"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role.ci_broker will be updated in-place
  ~ resource "aws_iam_role" "ci_broker" {
        id                    = "javabin-ci-broker"
        name                  = "javabin-ci-broker"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.compliance_reporter will be updated in-place
  ~ resource "aws_iam_role" "compliance_reporter" {
        id                    = "javabin-compliance-reporter"
        name                  = "javabin-compliance-reporter"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.cost_report will be updated in-place
  ~ resource "aws_iam_role" "cost_report" {
        id                    = "javabin-cost-report"
        name                  = "javabin-cost-report"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.daily_cost_check will be updated in-place
  ~ resource "aws_iam_role" "daily_cost_check" {
        id                    = "javabin-daily-cost-check"
        name                  = "javabin-daily-cost-check"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.override_cleanup will be updated in-place
  ~ resource "aws_iam_role" "override_cleanup" {
        id                    = "javabin-override-cleanup"
        name                  = "javabin-override-cleanup"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.password_set will be updated in-place
  ~ resource "aws_iam_role" "password_set" {
        id                    = "javabin-password-set"
        name                  = "javabin-password-set"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.resource_tagger will be created
  + resource "aws_iam_role" "resource_tagger" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-resource-tagger"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role.slack_alert will be updated in-place
  ~ resource "aws_iam_role" "slack_alert" {
        id                    = "javabin-slack-alert"
        name                  = "javabin-slack-alert"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.team_provisioner will be updated in-place
  ~ resource "aws_iam_role" "team_provisioner" {
        id                    = "javabin-team-provisioner"
        name                  = "javabin-team-provisioner"
      ~ tags                  = {
          - "project" = "javabin" -> null
        }
      ~ tags_all              = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role_policy.budget_enforcer will be created
  + resource "aws_iam_role_policy" "budget_enforcer" {
      + id          = (known after apply)
      + name        = "javabin-budget-enforcer"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/*"
                      + Sid      = "SSMRead"
                    },
                  + {
                      + Action    = [
                          + "ecs:ListServices",
                          + "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "ECSListAndDescribe"
                    },
                  + {
                      + Action   = "ecs:ListServices"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                      + Sid      = "ECSListServicesUnconstrained"
                    },
                  + {
                      + Action   = "ecs:UpdateService"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:service/javabin-platform/*"
                      + Sid      = "ECSUpdateService"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.resource_tagger will be created
  + resource "aws_iam_role_policy" "resource_tagger" {
      + id          = (known after apply)
      + name        = "javabin-resource-tagger"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "tag:TagResources",
                          + "tag:GetResources",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "TagResources"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.budget_enforcer_logs will be created
  + resource "aws_iam_role_policy_attachment" "budget_enforcer_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-budget-enforcer"
    }

  # module.lambdas.aws_iam_role_policy_attachment.resource_tagger_logs will be created
  + resource "aws_iam_role_policy_attachment" "resource_tagger_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-resource-tagger"
    }

  # module.lambdas.aws_lambda_function.apply_gate will be updated in-place
  ~ resource "aws_lambda_function" "apply_gate" {
        id                             = "javabin-apply-gate"
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.budget_enforcer will be created
  + resource "aws_lambda_function" "budget_enforcer" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/budget_enforcer.zip"
      + function_name                  = "javabin-budget-enforcer"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "i3E4tI5mT0qCS601izSv52QOgwAndKfcZ3JkLzxTNis="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
      + timeout                        = 60
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "BUDGET_NAME_PREFIX" = "javabin-team-"
              + "COST_WEBHOOK_PARAM" = "/javabin/slack/platform-cost-alerts-webhook"
              + "ECS_CLUSTER"        = "javabin-platform"
            }
        }
    }

  # module.lambdas.aws_lambda_function.ci_broker will be updated in-place
  ~ resource "aws_lambda_function" "ci_broker" {
        id                             = "javabin-ci-broker"
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.compliance_reporter will be updated in-place
  ~ resource "aws_lambda_function" "compliance_reporter" {
        id                             = "javabin-compliance-reporter"
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.cost_report will be updated in-place
  ~ resource "aws_lambda_function" "cost_report" {
        id                             = "javabin-cost-report"
      ~ last_modified                  = "2026-03-16T19:34:38.000+0000" -> (known after apply)
      ~ source_code_hash               = "tqOEmNCrENsRH68jr/pMXLCtzlzQDVeaRdyLn9y/TKo=" -> "esY+QEmRWxvAf+1NPbBdjCkq3eLoOeCDHEj67n8xG9Q="
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.daily_cost_check will be updated in-place
  ~ resource "aws_lambda_function" "daily_cost_check" {
        id                             = "javabin-daily-cost-check"
      ~ last_modified                  = "2026-03-16T19:34:51.000+0000" -> (known after apply)
      ~ source_code_hash               = "O1jtWocVLFewhv1wjT0tNRZbrReVg5HX2D6pjgkSu+M=" -> "BJb5rFoBUKg2rYSX/YVv30vdN5IwFS0S+RbcroWezXA="
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.override_cleanup will be updated in-place
  ~ resource "aws_lambda_function" "override_cleanup" {
        id                             = "javabin-override-cleanup"
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.password_set will be updated in-place
  ~ resource "aws_lambda_function" "password_set" {
        id                             = "javabin-password-set"
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.resource_tagger will be created
  + resource "aws_lambda_function" "resource_tagger" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/resource_tagger.zip"
      + function_name                  = "javabin-resource-tagger"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "lk/mypHvtYyrMSzY8lPGOhrKtJnu0FawUhidl+Nm+2Q="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
      + timeout                        = 30
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "AWS_ACCOUNT_ID" = "553637109631"
            }
        }
    }

  # module.lambdas.aws_lambda_function.securityhub_summary will be updated in-place
  ~ resource "aws_lambda_function" "securityhub_summary" {
        id                             = "javabin-securityhub-summary"
      ~ last_modified                  = "2026-03-16T21:42:32.000+0000" -> (known after apply)
      ~ source_code_hash               = "pU/wjCWqsyj/zHkvhqB/VrSJlkVtVQCaiMmrm0J2U3A=" -> "LEUBnHrV2lMxv4O53MGv3xojmz1Pwru9JCHtO6Kg4f0="
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.slack_alert will be updated in-place
  ~ resource "aws_lambda_function" "slack_alert" {
        id                             = "javabin-slack-alert"
      ~ last_modified                  = "2026-03-16T21:42:26.000+0000" -> (known after apply)
      ~ source_code_hash               = "pU/wjCWqsyj/zHkvhqB/VrSJlkVtVQCaiMmrm0J2U3A=" -> "LEUBnHrV2lMxv4O53MGv3xojmz1Pwru9JCHtO6Kg4f0="
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-03-16T19:34:25.000+0000" -> (known after apply)
      ~ source_code_hash               = "tfwZJi3BEYFmZVlD3jH52COLMJfFRHrNkRhYYGGgiUA=" -> "DOsqyX7kl+NoVN861D4rN0ieHND77GeWwpYD1A93gdQ="
      ~ tags                           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              - "ACCOUNT_ID"               = "553637109631"
              - "ALERTS_TOPIC_ARN"         = "arn:aws:sns:eu-central-1:553637109631:javabin-alerts"
              - "COGNITO_INTERNAL_POOL_ID" = "eu-central-1_Icikv3dtD"
              - "GITHUB_APP_ID_PARAM"      = "/javabin/platform/github-app-id"
              - "GITHUB_APP_KEY_PARAM"     = "/javabin/platform/github-app-key"
              - "GITHUB_ORG"               = "javaBin"
              - "GOOGLE_ADMIN_EMAIL_PARAM" = "/javabin/platform/google-admin-email"
              - "GOOGLE_SA_PARAM"          = "/javabin/platform/google-admin-sa"
              - "IDENTITY_STORE_ID"        = "d-9967444724"
              - "INFRA_WEBHOOK_PARAM"      = "/javabin/slack/platform-resource-alerts-webhook"
              - "PASSWORD_SET_URL_PARAM"   = "/javabin/platform/password-set-function-url"
              - "PROJECT"                  = "javabin"
              - "SIGNING_KEY_PARAM"        = "/javabin/platform/password-token-signing-key"
              - "SSO_INSTANCE_ARN"         = "arn:aws:sso:::instance/ssoins-6987654c95c9a069"
            } -> (known after apply)
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.budget_enforcer_sns will be created
  + resource "aws_lambda_permission" "budget_enforcer_sns" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-budget-enforcer"
      + id                  = (known after apply)
      + principal           = "sns.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowSNSBudgetEnforcement"
      + statement_id_prefix = (known after apply)
    }

  # module.lambdas.aws_lambda_permission.resource_tagger_eventbridge will be created
  + resource "aws_lambda_permission" "resource_tagger_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-resource-tagger"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

  # module.lambdas.aws_lb_listener_rule.password_set will be updated in-place
  ~ resource "aws_lb_listener_rule" "password_set" {
        id           = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:listener-rule/app/javabin-platform-alb/bec1dd43ab8341b9/500c9c2b4186bf45/38c91f257c910d33"
      ~ tags         = {
          - "project" = "javabin" -> null
        }
      ~ tags_all     = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (3 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.lambdas.aws_lb_target_group.password_set will be updated in-place
  ~ resource "aws_lb_target_group" "password_set" {
        id                                 = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:targetgroup/javabin-password-set/23a67a9e9f399518"
        name                               = "javabin-password-set"
      ~ tags                               = {
          - "project" = "javabin" -> null
        }
      ~ tags_all                           = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

  # module.lambdas.aws_sns_topic.budget_enforcement will be created
  + resource "aws_sns_topic" "budget_enforcement" {
      + arn                         = (known after apply)
      + beginning_archive_time      = (known after apply)
      + content_based_deduplication = false
      + fifo_throughput_scope       = (known after apply)
      + fifo_topic                  = false
      + id                          = (known after apply)
      + name                        = "javabin-budget-enforcement"
      + name_prefix                 = (known after apply)
      + owner                       = (known after apply)
      + policy                      = (known after apply)
      + signature_version           = (known after apply)
      + tags_all                    = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "javabin"
        }
      + tracing_config              = (known after apply)
    }

  # module.lambdas.aws_sns_topic_policy.budget_enforcement will be created
  + resource "aws_sns_topic_policy" "budget_enforcement" {
      + arn    = (known after apply)
      + id     = (known after apply)
      + owner  = (known after apply)
      + policy = (known after apply)
    }

  # module.lambdas.aws_sns_topic_subscription.budget_enforcer will be created
  + resource "aws_sns_topic_subscription" "budget_enforcer" {
      + arn                             = (known after apply)
      + confirmation_timeout_in_minutes = 1
      + confirmation_was_authenticated  = (known after apply)
      + endpoint                        = (known after apply)
      + endpoint_auto_confirms          = false
      + filter_policy_scope             = (known after apply)
      + id                              = (known after apply)
      + owner_id                        = (known after apply)
      + pending_confirmation            = (known after apply)
      + protocol                        = "lambda"
      + raw_message_delivery            = false
      + topic_arn                       = (known after apply)
    }

  # module.lambdas.aws_ssm_parameter.password_set_function_url will be updated in-place
  ~ resource "aws_ssm_parameter" "password_set_function_url" {
        id        = "/javabin/platform/password-set-function-url"
        name      = "/javabin/platform/password-set-function-url"
      ~ tags      = {
          - "project" = "javabin" -> null
        }
      ~ tags_all  = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (6 unchanged attributes hidden)
    }

  # module.monitoring.aws_ce_anomaly_monitor.main will be updated in-place
  ~ resource "aws_ce_anomaly_monitor" "main" {
        id                = "arn:aws:ce::553637109631:anomalymonitor/3609b3f1-c834-444e-a218-02ac6da1cb4d"
        name              = "javabin-anomaly-monitor"
      ~ tags              = {
          - "project" = "javabin" -> null
        }
      ~ tags_all          = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (3 unchanged attributes hidden)
    }

  # module.monitoring.aws_ce_anomaly_subscription.alerts will be updated in-place
  ~ resource "aws_ce_anomaly_subscription" "alerts" {
        id               = "arn:aws:ce::553637109631:anomalysubscription/f6b079c9-5174-43b7-85f3-dde533995482"
        name             = "javabin-anomaly-alerts"
      ~ tags             = {
          - "project" = "javabin" -> null
        }
      ~ tags_all         = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["commit"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "commit"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["created-by"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "created-by"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["environment"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "environment"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["managed-by"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "managed-by"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["repo"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "repo"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["service"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "service"
      + type    = (known after apply)
    }

  # module.monitoring.aws_ce_cost_allocation_tag.tags["team"] will be created
  + resource "aws_ce_cost_allocation_tag" "tags" {
      + id      = (known after apply)
      + status  = "Active"
      + tag_key = "team"
      + type    = (known after apply)
    }

  # module.monitoring.aws_cloudwatch_event_rule.config_compliance will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "config_compliance" {
        id             = "javabin-config-compliance-change"
        name           = "javabin-config-compliance-change"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.console_login will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "console_login" {
        id             = "javabin-console-login"
        name           = "javabin-console-login"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.guardduty_findings will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "guardduty_findings" {
        id             = "javabin-guardduty-findings"
        name           = "javabin-guardduty-findings"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.iam_changes will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "iam_changes" {
        id             = "javabin-iam-changes"
        name           = "javabin-iam-changes"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.resource_creation will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "resource_creation" {
        id             = "javabin-resource-creation"
        name           = "javabin-resource-creation"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.resource_modification will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "resource_modification" {
        id             = "javabin-resource-modification"
        name           = "javabin-resource-modification"
      ~ tags           = {
          - "project" = "javabin" -> null
        }
      ~ tags_all       = {
          - "project"     = "javabin" -> null
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
            # (3 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.securityhub_findings will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "securityhub_findings" {

LLM Review

Risk: 🟢 LOW

Routine infrastructure update with tag standardization, new Lambda functions for budget enforcement and resource tagging, and IAM policy refinements.

  • [routine] Widespread tag updates removing 'project=javabin' and adding 'repo', 'service', 'environment', 'managed-by', 'team' tags across 50+ resources for improved cost allocation and resource tracking.
  • [routine] Two new Lambda functions created: budget_enforcer (monitors ECS service costs via SNS) and resource_tagger (auto-tags resources on creation via CloudTrail events). Both have minimal permissions scoped to their functions.
  • [routine] IAM policy refinement for ci_team deny policy: removed blanket SNS/S3 denies and replaced with resource-specific restrictions on javabin SNS topics and terraform state buckets, improving least-privilege.
  • [routine] Lambda function code updates (cost_report, daily_cost_check, securityhub_summary, slack_alert, team_provisioner) with new source code hashes; team_provisioner environment variables being cleared (likely moved to SSM).
  • 💰 [cost] New SNS topic 'javabin-budget-enforcement' created for budget alerts; minimal cost impact but adds new messaging infrastructure for cost monitoring.

@Alexanderamiri Alexanderamiri merged commit 111106f into main Mar 17, 2026
3 checks passed
@Alexanderamiri Alexanderamiri deleted the feat/tags-alerts-rds-budget branch March 17, 2026 19:18
Alexanderamiri added a commit that referenced this pull request Mar 17, 2026
)

## Summary
Fixes apply failure from #77.

- **Permission boundary**: `budget-enforcer` and `resource-tagger` IAM
roles were missing `permissions_boundary`. The self-replicating boundary
on `ci-infra` blocks role creation without it.
- **Cost allocation tags**: `repo`, `created-by`, `commit` tags don't
exist on any billed resource yet — AWS rejects activation. Defer to
phase 2/3 (activate after resources with those tags exist).

## Test plan
- [ ] `terraform plan` shows 2 role modifications (add boundary) + 3 tag
removals
- [ ] Apply succeeds
Alexanderamiri added a commit that referenced this pull request Mar 17, 2026
## Summary
Adds `DenyPlatformSecurityGroups` to the developer permission boundary.
Denies modify/delete operations on security groups named `javabin-*`
(platform ALB and ECS tasks SGs).

Teams can still create their own SGs (needed for RDS module — e.g.,
`moresleep-rds-sg`).

Addresses security review finding from #77: SG operations were removed
from the team deny policy to support RDS. The boundary now protects
platform SGs while allowing team SG creation.

## Test plan
- [ ] `terraform plan` shows boundary policy update
- [ ] Apply succeeds
- [ ] Team role cannot modify `javabin-alb-sg` or `javabin-ecs-tasks-sg`
- [ ] Team role can create `{app}-rds-sg` via expanded TF
Alexanderamiri added a commit that referenced this pull request May 9, 2026
)

## Summary

Foundation changes to make tags a first-class primitive for ABAC, cost
attribution, and auditability. Plus alert enrichment, budget
enforcement, and RDS support.

### Tags as primitive
- **Tag schema**: 5 static tags (team, service, repo, environment,
managed-by) via provider `default_tags`. Dropped `project` tag (always
"javabin", zero information).
- **Resource tagger Lambda**: EventBridge-triggered (wildcard
`{"prefix": "aws."}` match), auto-tags `created-by` + `commit` from
CloudTrail session names. Tags added via AWS API — invisible to
Terraform, no drift.
- **Cost allocation tags**: Activated for all 7 tag keys so Cost
Explorer can group by team/service.
- **ECS tag propagation**: `propagate_tags = SERVICE` so Fargate task
costs are attributed to teams.

### Alert enrichment (Task A)
- CI session names changed to `{actor}-{sha8}-{run_id}` in all 4
workflows.
- `slack_alert` `parse_identity()` extracts actor/commit from new
format.
- Cost reports (daily + weekly) include per-team tag breakdown.

### Budget enforcement (Task D)
- New `budget-enforcer` Lambda: scales ECS services to `desired_count=0`
at 200% budget.
- `team_provisioner` adds 200% notification alongside existing 80%.

### RDS module (Task E)
- New `service-rds` module: PostgreSQL with Secrets Manager password,
private subnets, ECS SG ingress.
- `registry.py` + `expand-modules.py` updated with engine-based routing
(`postgres` vs `dynamodb`).

### IAM restructure
- Team deny policy: ABAC (ARN-scoped) where AWS supports tags (SNS, S3,
ECS, ELB). Explicit denies only where AWS lacks tag conditions (EC2 VPC,
GuardDuty, SecurityHub, Config, CloudTrail, Organizations, IAM).
- `service-role` module: configurable `trusted_services`
(ECS/EC2/Lambda).
- EventBridge resource-tagger uses wildcard; monitoring rules keep
curated lists (documented volume rationale).

## Test plan
- [ ] `terraform plan` shows tag migration (project removed,
service+repo added) on existing resources
- [ ] After apply: resources show 5 Terraform-managed tags in console
- [ ] Trigger CI run → Slack alert shows actor name + commit link
- [ ] Next day: Cost Explorer GroupBy `team` returns per-team costs
- [ ] Create test resource → resource-tagger tags it within 15 min
- [ ] Invoke budget-enforcer with test payload → ECS service scales to 0
- [ ] Test app with `engine: postgres` in app.yaml → RDS created in
private subnet
Alexanderamiri added a commit that referenced this pull request May 9, 2026
)

## Summary
Fixes apply failure from #77.

- **Permission boundary**: `budget-enforcer` and `resource-tagger` IAM
roles were missing `permissions_boundary`. The self-replicating boundary
on `ci-infra` blocks role creation without it.
- **Cost allocation tags**: `repo`, `created-by`, `commit` tags don't
exist on any billed resource yet — AWS rejects activation. Defer to
phase 2/3 (activate after resources with those tags exist).

## Test plan
- [ ] `terraform plan` shows 2 role modifications (add boundary) + 3 tag
removals
- [ ] Apply succeeds
Alexanderamiri added a commit that referenced this pull request May 9, 2026
## Summary
Adds `DenyPlatformSecurityGroups` to the developer permission boundary.
Denies modify/delete operations on security groups named `javabin-*`
(platform ALB and ECS tasks SGs).

Teams can still create their own SGs (needed for RDS module — e.g.,
`moresleep-rds-sg`).

Addresses security review finding from #77: SG operations were removed
from the team deny policy to support RDS. The boundary now protects
platform SGs while allowing team SG creation.

## Test plan
- [ ] `terraform plan` shows boundary policy update
- [ ] Apply succeeds
- [ ] Team role cannot modify `javabin-alb-sg` or `javabin-ecs-tasks-sg`
- [ ] Team role can create `{app}-rds-sg` via expanded TF
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