Skip to content

feat(org): let developers set their own team's app secrets - #139

Merged
Alexanderamiri merged 4 commits into
mainfrom
feat/secrets-selfservice
Oct 23, 2026
Merged

Alexanderamiri merged 4 commits into
mainfrom
feat/secrets-selfservice

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Why

app.yaml resources.secrets are created with the value PLACEHOLDER, but the javabin-developer permission set can only read /javabin/apps/<team>/*, so no team can give its secrets real values. cupcake needs SP_USER, SP_PASSWORD, BRING_API_USER and BRING_API_KEY.

What changed

  • terraform/org/identity-center.tf, javabin-developer (human-applied):
    • ssm:PutParameter, LabelParameterVersion and GetParameterHistory on /javabin/apps/${PrincipalTag/team}/*, only for parameters tagged with that team. That means only secrets the platform created; a hand-made parameter would break the app's next terraform apply.
    • An explicit Deny on */cognito-client-*, */auth and */db-master-password, which are platform-managed.
  • New Lambda javabin-app-secret-rollout (CI-applied): on an SSM Update under /javabin/apps/, it forces a new deployment of {team}-{app}, because ECS reads secrets only at task start. Developers get no ecs:UpdateService: that action would also let them change networking and task definitions.
  • Docs: app-yaml-reference, lambda-functions, CLAUDE.md. The CLI side is feat(secret): set app.yaml secrets from the CLI javabin-cli#6, javabin secret list/set.

Applying

  • Merging applies the Lambda through platform CI.
  • terraform/org must be applied by hand: cd terraform/org && terraform plan && terraform apply (javabin admin). It should show one in-place update to aws_ssoadmin_permission_set_inline_policy.developer.

Known limit

A session carries a single team tag. A hero in several teams can manage only the team their session is tagged with.

Checks

  • Lambda tests pass, including the new test_app_secret_rollout.py.
  • terraform validate passes on terraform/platform and terraform/org.
  • An independent review agent's findings are addressed: the tag condition, the db-master-password deny, and dropping UpdateService.

🤖 Generated with Claude Code

app.yaml resources.secrets are created with a PLACEHOLDER value, but the
developer permission set could only read /javabin/apps/<team>/*, so no
team could set its own secrets. Allow ssm:PutParameter on the team's own
path, deny writes to the platform-managed Cognito credentials and auth
requests, and allow ecs:UpdateService on the team's own services so
tasks can be restarted to pick up new values. Used by javabin secret set
in javaBin/javabin-cli.

terraform/org is human-applied.

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

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 72 to add, 27 to change, 1 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

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.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"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "iam:DeleteRolePolicy",
                          + "iam:UpdateAssumeRolePolicy",
                            "iam:PutRolePolicy",
                            # (4 unchanged elements hidden)
                            "sns:SetTopicAttributes",
                          + "ec2:AuthorizeSecurityGroup*",
                          + "ec2:RevokeSecurityGroup*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action    = [
                            "ec2:DeleteTags",
                            "ec2:CreateTags",
                            "ecs:TagResource",
                            "ecs:UntagResource",
                            "elasticloadbalancing:AddTags",
                            "elasticloadbalancing:RemoveTags",
                            "iam:TagRole",
                            "iam:UntagRole",
                            "sns:TagResource",
                            "sns:UntagResource",
                        ]
                        Condition = {
                            StringEquals = {
                                "aws:ResourceTag/team" = [
                                    "shared",
                                    "platform",
                                ]
                            }
                        }
                        Effect    = "Deny"
                        Resource  = "*"
                        Sid       = "DenyTagStrip"
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                  ~ {
                      ~ NotResource = [
                            "arn:aws:iam::*:role/${aws:PrincipalTag/team}-*",
                          - "arn:aws:iam::*:role/javabin-ecs-execution",
                          + "arn:aws:iam::*:role/javabin-ecs-exec-${aws:PrincipalTag/team}",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action      = [
                            "logs:GetLogEvents",
                            "logs:FilterLogEvents",
                            "logs:StartQuery",
                            "logs:GetQueryResults",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*",
                            "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*:*",
                        ]
                        Sid         = "DenyXLogs"
                    },
                    # (3 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_role.ecs_exec_team["cake"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-cake"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-cake"
          + "for-team" = "cake"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-cake"
          + "environment" = "production"
          + "for-team"    = "cake"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["commandchimps"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-commandchimps"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-commandchimps"
          + "for-team" = "commandchimps"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-commandchimps"
          + "environment" = "production"
          + "for-team"    = "commandchimps"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["cupcake"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-cupcake"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-cupcake"
          + "for-team" = "cupcake"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-cupcake"
          + "environment" = "production"
          + "for-team"    = "cupcake"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["developer"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-developer"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-developer"
          + "for-team" = "developer"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-developer"
          + "environment" = "production"
          + "for-team"    = "developer"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["java-no"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-java-no"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-java-no"
          + "for-team" = "java-no"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-java-no"
          + "environment" = "production"
          + "for-team"    = "java-no"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["javazone-no"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-javazone-no"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-javazone-no"
          + "for-team" = "javazone-no"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-javazone-no"
          + "environment" = "production"
          + "for-team"    = "javazone-no"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["javazone-work"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-javazone-work"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-javazone-work"
          + "for-team" = "javazone-work"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-javazone-work"
          + "environment" = "production"
          + "for-team"    = "javazone-work"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["jzwatch"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-jzwatch"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-jzwatch"
          + "for-team" = "jzwatch"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-jzwatch"
          + "environment" = "production"
          + "for-team"    = "jzwatch"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["owners"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-owners"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-owners"
          + "for-team" = "owners"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-owners"
          + "environment" = "production"
          + "for-team"    = "owners"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["partnermanagement"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-partnermanagement"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-partnermanagement"
          + "for-team" = "partnermanagement"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-partnermanagement"
          + "environment" = "production"
          + "for-team"    = "partnermanagement"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-apps"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-apps"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-apps"
          + "for-team" = "team-apps"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-apps"
          + "environment" = "production"
          + "for-team"    = "team-apps"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-cibus"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-cibus"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-cibus"
          + "for-team" = "team-cibus"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-cibus"
          + "environment" = "production"
          + "for-team"    = "team-cibus"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-cibus2"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-cibus2"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-cibus2"
          + "for-team" = "team-cibus2"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-cibus2"
          + "environment" = "production"
          + "for-team"    = "team-cibus2"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-ems"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-ems"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-ems"
          + "for-team" = "team-ems"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-ems"
          + "environment" = "production"
          + "for-team"    = "team-ems"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-incogito"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-incogito"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-incogito"
          + "for-team" = "team-incogito"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-incogito"
          + "environment" = "production"
          + "for-team"    = "team-incogito"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-javazone-badge"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-javazone-badge"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-javazone-badge"
          + "for-team" = "team-javazone-badge"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-javazone-badge"
          + "environment" = "production"
          + "for-team"    = "team-javazone-badge"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-javazone-video"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-javazone-video"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-javazone-video"
          + "for-team" = "team-javazone-video"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-javazone-video"
          + "environment" = "production"
          + "for-team"    = "team-javazone-video"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-jz-text-adventure"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-jz-text-adventure"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-jz-text-adventure"
          + "for-team" = "team-jz-text-adventure"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-jz-text-adventure"
          + "environment" = "production"
          + "for-team"    = "team-jz-text-adventure"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-portal"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-portal"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-portal"
          + "for-team" = "team-portal"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-portal"
          + "environment" = "production"
          + "for-team"    = "team-portal"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-scala-training-code"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-scala-training-code"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-scala-training-code"
          + "for-team" = "team-scala-training-code"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-scala-training-code"
          + "environment" = "production"
          + "for-team"    = "team-scala-training-code"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["team-submitit"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-team-submitit"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-team-submitit"
          + "for-team" = "team-submitit"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-team-submitit"
          + "environment" = "production"
          + "for-team"    = "team-submitit"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["teammoosehead"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-teammoosehead"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-teammoosehead"
          + "for-team" = "teammoosehead"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-teammoosehead"
          + "environment" = "production"
          + "for-team"    = "teammoosehead"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["teamteknologihuset"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-teamteknologihuset"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-teamteknologihuset"
          + "for-team" = "teamteknologihuset"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-teamteknologihuset"
          + "environment" = "production"
          + "for-team"    = "teamteknologihuset"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ecs_exec_team["testteam"] will be created
  + resource "aws_iam_role" "ecs_exec_team" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ecs-tasks.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-ecs-exec-testteam"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags                  = {
          + "Name"     = "javabin-ecs-exec-testteam"
          + "for-team" = "testteam"
          + "team"     = "shared"
        }
      + tags_all              = {
          + "Name"        = "javabin-ecs-exec-testteam"
          + "environment" = "production"
          + "for-team"    = "testteam"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["cake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-cake:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "cake"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/cake-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["commandchimps"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-commandchimps:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "commandchimps"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/commandchimps-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["cupcake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-cupcake:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "cupcake"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/cupcake-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["developer"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-developer:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "developer"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/developer-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["java-no"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-java-no:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "java-no"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/java-no-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["javazone-no"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-javazone-no:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "javazone-no"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/javazone-no-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["javazone-work"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-javazone-work:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "javazone-work"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
                  - {
                      - Action   = [
                          - "ecs:DescribeTaskDefinition",
                          - "ecs:RegisterTaskDefinition",
                          - "ecs:DeregisterTaskDefinition",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EcsTaskDefinitionManagement"
                    },
                  - {
                      - Action   = [
                          - "iam:PassRole",
                        ]
                      - Effect   = "Allow"
                      - Resource = [
                          - "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/javazone-work-*",
                        ]
                      - Sid      = "PassRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["jzwatch"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-jzwatch:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = [
                          - "ecs:UpdateService",
                          - "ecs:DescribeServices",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "jzwatch"
                            }
                        }
                      - Effect    = "Allow"
                      - Resource  = "*"
                      - Sid       = "EcsServiceManagement"
                    },
      

LLM Review

Risk: 🔴 HIGH

The plan modifies the developer permissions boundary (adding EC2 security group authorize/revoke and iam:UpdateAssumeRolePolicy to allowed actions, and changing the ECS execution role exemption), and creates many new per-team ECS execution IAM roles, all of which need security review before apply.

  • 🔒 [security] The developer boundary policy (javabin-developer-boundary) is changed to allow ec2:AuthorizeSecurityGroup* and ec2:RevokeSecurityGroup* and iam:UpdateAssumeRolePolicy. These are permissive additions to a guardrail policy. Security group changes could open ingress to the internet, and UpdateAssumeRolePolicy could let a developer rewrite trust policies. Confirm this is intended and that the boundary still prevents privilege escalation.
  • 🔒 [security] The boundary's NotResource exemption for the ECS execution role changes from javabin-ecs-execution to javabin-ecs-exec-${aws:PrincipalTag/team}. This is a change to the role-scoping logic for IAM operations and should be verified so teams can only touch their own exec role.
  • 🔒 [security] About 30 new aws_iam_role.ecs_exec_team instances are created per team (e.g. javabin-ecs-exec-cake), each tagged team=shared with permissions_boundary javabin-org-boundary. Verify the attached policies and that the team tag and for-team tag do not conflict with the boundary's team-based conditions.
  • ✅ [routine] The visible portion of the plan is truncated, so the full set of creates and any destroys is not confirmed. No resource destruction is visible in the shown output. Review the complete plan for any deletes before applying.
  • 💰 [cost] New IAM roles and policies are free. No NAT gateways, RDS instances, or load balancers appear in the visible plan, so no direct new billable spend is evident.

Alexanderamiri added a commit to javaBin/javabin-cli that referenced this pull request Oct 23, 2026
…Ctrl-C

- set checks the parameter exists first and refuses to create it
  (the platform creates declared secrets on deploy)
- remove --restart / ECS UpdateService; the platform restarts the
  service automatically when a secret changes (javaBin/platform#139)
- restore the terminal if interrupted during the hidden prompt
- document why list decrypts; update CLAUDE.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review feedback on #139:
- PutParameter requires the parameter's team tag to match, so developers
  can only set secrets the platform created; creating one by hand would
  break the app's next terraform apply (ParameterAlreadyExists).
- db-master-password joins the platform-managed deny list.
- Drop ecs:UpdateService from the developer permission set: it also
  allows changing networking and task definitions. Instead a new
  javabin-app-secret-rollout Lambda forces a new deployment of
  {team}-{app} when one of its secrets is updated (EventBridge).

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

Copy link
Copy Markdown

Terraform Plan

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

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

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

Terraform will perform the following actions:

  # module.lambdas.aws_cloudwatch_event_rule.app_secret_rollout_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_secret_rollout_trigger" {
      + arn            = (known after apply)
      + description    = "An app secret value was changed"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + prefix = "/javabin/apps/"
                        },
                    ]
                  + operation = [
                      + "Update",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-secret-rollout-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

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

  # module.lambdas.aws_iam_role.app_secret_rollout will be created
  + resource "aws_iam_role" "app_secret_rollout" {
      + 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-app-secret-rollout"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_secret_rollout will be created
  + resource "aws_iam_role_policy" "app_secret_rollout" {
      + id          = (known after apply)
      + name        = "javabin-app-secret-rollout"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "ecs:UpdateService"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:service/javabin-platform/*"
                      + Sid      = "RestartAppServices"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

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

  # module.lambdas.aws_lambda_function.app_secret_rollout will be created
  + resource "aws_lambda_function" "app_secret_rollout" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_secret_rollout.zip"
      + function_name                  = "javabin-app-secret-rollout"
      + 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               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 30
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "PROJECT" = "javabin"
            }
        }
    }

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

Plan: 7 to add, 0 to change, 0 to destroy.

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

Saved the plan to: tfplan

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

LLM Review

Risk: 🟢 LOW

Plan creates 7 new resources (no changes or destroys) for an EventBridge-triggered Lambda that restarts app ECS services when /javabin/apps/ SSM parameters are updated.

  • 🔒 [security] The IAM role policy grants only ecs:UpdateService (Effect Allow) scoped to arn:aws:ecs:eu-central-1:553637109631:service/javabin-platform/*. Scope is narrow and no internet exposure or security group changes are involved. The role has the org permissions boundary attached (javabin-org-boundary), which is good.
  • 🔒 [security] The EventBridge rule matches SSM Parameter Store 'Update' events under the /javabin/apps/ prefix. Confirm the Lambda only reads the event and doesn't need access to parameter values, since it has no SSM read permissions in this plan.
  • 💰 [cost] All new resources are low-cost or free: one Lambda (128 MB, 30s timeout, invoked only on parameter updates), one EventBridge rule and target, and IAM objects. No NAT gateways, RDS, or load balancers are introduced.
  • ✅ [routine] The Lambda is packaged from a local zip (lambdas/builds/app_secret_rollout.zip) with source_code_hash set. Ensure the zip is built from the reviewed commit before apply. The runtime is python3.12, which is supported.
  • ✅ [routine] The Lambda has no reserved concurrency limit (-1, unreserved). This is acceptable for an event-driven function, but a burst of parameter updates could trigger many ECS UpdateService calls.

@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
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

Terraform will perform the following actions:

  # module.lambdas.aws_cloudwatch_event_rule.app_secret_rollout_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_secret_rollout_trigger" {
      + arn            = (known after apply)
      + description    = "An app secret value was changed"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + prefix = "/javabin/apps/"
                        },
                    ]
                  + operation = [
                      + "Update",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-secret-rollout-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

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

  # module.lambdas.aws_iam_role.app_secret_rollout will be created
  + resource "aws_iam_role" "app_secret_rollout" {
      + 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-app-secret-rollout"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_secret_rollout will be created
  + resource "aws_iam_role_policy" "app_secret_rollout" {
      + id          = (known after apply)
      + name        = "javabin-app-secret-rollout"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "ecs:UpdateService"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:service/javabin-platform/*"
                      + Sid      = "RestartAppServices"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

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

  # module.lambdas.aws_lambda_function.app_secret_rollout will be created
  + resource "aws_lambda_function" "app_secret_rollout" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_secret_rollout.zip"
      + function_name                  = "javabin-app-secret-rollout"
      + 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               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 30
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "PROJECT" = "javabin"
            }
        }
    }

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

Plan: 7 to add, 0 to change, 0 to destroy.

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

Saved the plan to: tfplan

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

LLM Review

Risk: 🟢 LOW

Plan creates 7 new resources for an EventBridge-triggered Lambda (app_secret_rollout) that restarts ECS services when app secrets in Parameter Store change; no existing resources are modified or destroyed.

  • 🔒 [security] The Lambda's IAM role has a scoped least-privilege policy: a single ecs:UpdateService action with Effect=Allow, limited to service/javabin-platform/* in eu-central-1. It also has the org permissions boundary (javabin-org-boundary) and only the AWSLambdaBasicExecutionRole managed policy for logs. No public access, security group, or wildcard IAM concerns.
  • 🔒 [security] The EventBridge rule matches SSM Parameter Store Change events with operation Update under the /javabin/apps/ prefix and invokes the Lambda via a permission scoped to events.amazonaws.com. Only an Update triggers the restart, so Create or Delete of parameters will not. Scope is appropriate.
  • 💰 [cost] All new resources are low-cost: a Lambda (128 MB, 30s timeout, invoked only on secret updates), an EventBridge rule and target, and an IAM role and policies. No NAT gateways, RDS, or load balancers. Expected cost is negligible.
  • ✅ [routine] The Lambda uses the python3.12 runtime from a local zip (lambdas/builds/app_secret_rollout.zip) with source_code_hash set. Confirm the zip is built from the intended commit before applying. The function is not versioned (publish=false) and has no reserved concurrency, which is fine for an infrequent event-driven job.
  • ✅ [routine] The ecs:UpdateService grant lacks a condition restricting the target cluster beyond the resource ARN, and the rule has no dead-letter queue or retry configuration. Both are low-severity and can be addressed in a follow-up. No destructive actions are present.

@Alexanderamiri
Alexanderamiri merged commit 833aa71 into main Oct 23, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the feat/secrets-selfservice branch October 23, 2026 22:10
Alexanderamiri added a commit to javaBin/javabin-cli that referenced this pull request Oct 23, 2026
* feat(secret): set app.yaml secrets from the CLI

Secrets declared in app.yaml resources.secrets are created with a
PLACEHOLDER value, and developers had no way to set the real one.
javabin secret list shows which still hold PLACEHOLDER (never the
values); javabin secret set <name> stores a value as an SSM SecureString
under /javabin/apps/<team>/<app>/, prompting without echo or reading
--stdin, and --restart forces a new ECS deployment so tasks pick it up.
Only names declared in app.yaml are accepted.

Needs the developer permission set change in javaBin/platform
(terraform/org, human-applied).

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

* fix(secret): never create parameters, drop --restart, restore tty on Ctrl-C

- set checks the parameter exists first and refuses to create it
  (the platform creates declared secrets on deploy)
- remove --restart / ECS UpdateService; the platform restarts the
  service automatically when a secret changes (javaBin/platform#139)
- restore the terminal if interrupted during the hidden prompt
- document why list decrypts; update CLAUDE.md

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Alexanderamiri added a commit that referenced this pull request Oct 24, 2026
…en in (#145)

Fixes #144.

## Why
- **Verified in CloudTrail:** every Identity Center session carries
`principalTags: {team: ""}` on `AssumeRoleWithSAML`, for
`javabin-team-cupcake`, `javabin-team-jzwatch` and `javabin-admin`
alike.
- **Cause:** the ABAC mapping reads `${path:enterprise.team}`, and
nothing ever sets that attribute on users.
- **Effect:** the per-team permission sets attach
`javabin-developer-boundary` as a policy, and its `DenyXTeam` compares
each resource's `team` tag with that empty principal tag. So developers
are denied everything team-scoped: their own app secrets (`javabin
secret list/set`), `/ecs/<team>/*` logs, and their ECS services.

## What changed
- **`terraform/platform/iam/boundary.tf`:**
  - The boundary JSON moves into a local.
- New `javabin-team-{team}-guardrails` per registered team: the same
rules with `${aws:PrincipalTag/team}` replaced by the team name.
- The guardrails are tagged `team=shared`, and `DenyBndMod` now also
covers `javabin-team-*-guardrails`, so team CI roles can't rewrite them.
  - Rendered size is 5,553 bytes, under 6,144.
- **team-provisioner, for `javabin-team-{team}`:**
- Attaches `javabin-team-{team}-guardrails` and detaches the shared
boundary.
- The inline policy now denies writes to platform-managed parameters
(`cognito-client-*`, `auth`, `db-master-password`).
- It also denies creating parameters that don't exist yet: an untagged
parameter would break the app's next `terraform apply`.
- It re-provisions the permission set so the change reaches the account.
- **IAM for the provisioner:** `Detach…`, `ProvisionPermissionSet`,
`DescribePermissionSetProvisioningStatus`, and `iam:GetPolicy` on the
guardrail policies.
- **Docs:** CLAUDE.md (known issue, naming) and app-yaml-reference
(which SSO role to use for secrets).

The ABAC `javabin-developer` permission set in `terraform/org` is still
unable to reach team resources. That's documented rather than fixed
here, because the per-team sets are what people use. #139's
`terraform/org` change targets that set, so it doesn't need applying for
secrets self-service.

## After merge
1. Platform CI creates the guardrail policies.
2. Run **Provision Teams** in javaBin/registry (manual run), which
attaches them and re-provisions the permission sets.
3. Sign in again with `javabin-team-cupcake`, then `javabin secret
list`.

## Checks
- The Lambda tests pass, including a new one for attach/detach, the deny
statements and reprovisioning.
- `terraform validate` passes.
- `terraform console` confirms the substitution renders the team name in
both the conditions and the ARNs.

## Guard overrides
- `aws route53 change-resource-record-sets --hosted-zone-id
Z09335963LMV0Z5QB9L45 --change-batch
file:///tmp/cupcake-cname-delete.json` — deleted the old
`cupcake.javazone.no` CNAME (→ `d2cxhqskuuhit7.cloudfront.net`) to cut
frosting over to the platform, on the user's explicit instruction. Not
part of this PR's code; listed because it ran from this session.

🤖 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 24, 2026
…ole plan (#147)

## Why

Reviews kept saying "Plan is truncated; the remaining changes ... could
not be reviewed". Where the input was actually cut:

| Reviews | Cut by | Real? |
|---|---|---|
| #134, #135 (14:14, 14:31), #137, #138, #139 on 2026-10-23, before #141
merged at 15:43 | the old 80k cut in `review-plan.py` | yes. These
include the quoted "other roles, Lambdas, or networking" review (#135,
14:31) |
| #135 on 2026-10-24 09:53 (run 40083680062) | #141's 400k cap: the plan
was 830k chars, 231 destroys (destroy diffs print every attribute) |
yes. The model saw 28 of 231 changes and correctly said so |
| #145 (2026-10-24 09:25, 09:40), #146, app reviews (cupcake#258,
frosting#249/#250) | none | no truncation claims |

Ruled out:
- (a) The PR comment's 60k cut: the review reads `plan-output.txt`
itself. The cut was silent to humans, though, so the comment now says
when it is cut.
- (b) `run-plan.sh` writes the full `-no-color` output with no
head/tail.
- (c) platform-ci reviews `terraform/platform/plan-output.txt`, the full
file.
- (e) No reviews hit `max_tokens`. Converse is still called without
inferenceConfig (no maxTokens set, on purpose), but a `stopReason:
max_tokens` answer is now discarded instead of trusted.
- (f) The Lambda reads the whole S3 object.

## What changed

- `scripts/review-plan.py`: no default input cap. The whole plan goes to
Haiku 5.5, which has a 1M-token context. `REVIEW_MAX_PLAN_CHARS` still
turns on the existing notice + change-list mode if it is set.
- The prompt now opens with `COMPLETE PLAN` (change count + the `Plan:`
line) and says not to call the plan truncated. It also explains that
Terraform hides parts of every plan by design: `# (N unchanged
attributes/blocks/elements hidden)`, `(sensitive value)`, `(known after
apply)` and partial `jsonencode` diffs are not truncation.
- `stopReason == "max_tokens"` → returns None. Platform CI records
FAILED; the Lambda retries.
- `scripts/post-plan-comment.sh`: adds a notice when the plan is cut at
60k (GitHub's comment limit) and keeps the cut-off point configurable.
- Docs: `docs/ci-workflow.md`.

Trade-off: a plan larger than the model's context now fails the review
(FAILED, override available) instead of being cut.

## Checks

- `python3 -m unittest discover -s scripts/tests`: 38 OK. New tests: the
complete-plan declaration, the elision markers, no default cap (2M-char
plan), an 830k destroy plan sent whole, max_tokens discarded, and the
comment cut notice.
- `python3 -m unittest discover -s terraform/lambda-src/tests`: 63 OK.
New tests: COMPLETE PLAN reaches Bedrock, and a max_tokens answer is
retried.
- `terraform fmt -check -recursive terraform`: OK. `shellcheck
scripts/post-plan-comment.sh`: OK. No workflow changes.

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

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