fix(iam): scope staging githubdeploy uploads to the staging release prefix (#158)
Some checks are pending
Architecture and changed-file quality / architecture (push) Waiting to run
Terraform CI / terraform (push) Waiting to run
Backend CI / Build and test (push) Waiting to run

This commit is contained in:
Adam Moussa 2026-09-18 17:10:13 -04:00 • committed by GitHub
parent 9eb51b528f
commit 4fb4159df2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 4 deletions

View file

@ -15,7 +15,10 @@ shared or Elastic Beanstalk-generated infrastructure.
The shared `shoc-backend` Elastic Beanstalk application and The shared `shoc-backend` Elastic Beanstalk application and
`shoc-sqlserver-shared` RDS instance, VPC, subnets, EB service role, shared `shoc-sqlserver-shared` RDS instance, VPC, subnets, EB service role, shared
certificate, shared RDS security group, and EB-generated SG/ALB/ASG/CloudFormation certificate, shared RDS security group, and EB-generated SG/ALB/ASG/CloudFormation
resources must never enter an environment state. resources must never enter an environment state. Both roots leave
`instance_security_group_id` null: the AWS provider reports the EB-generated
`awseb-*-AWSEBSecurityGroup` as an empty `SecurityGroups` setting, so pinning it
produces a permanent update diff.
Secret values are not Terraform resources, variables, outputs, or managed EB Secret values are not Terraform resources, variables, outputs, or managed EB
settings. Terraform manages the app-config secret shell and maps approved JSON settings. Terraform manages the app-config secret shell and maps approved JSON
@ -132,7 +135,10 @@ exact bundle it uploads; bundle bytes never enter Terraform plans or state.
GitHub Actions owns application versions. It compiles the bundle, uploads it, GitHub Actions owns application versions. It compiles the bundle, uploads it,
creates the Elastic Beanstalk application version, and calls creates the Elastic Beanstalk application version, and calls
`UpdateEnvironment`. Terraform ignores `version_label` so those deploys are not `UpdateEnvironment`. Terraform ignores `version_label` so those deploys are not
drift. If health, smoke, or the webhook probe fails after that update, the job drift. The non-legacy deploy policy writes bundles only under
`shoc-backend/releases/<environment>/*`; the Elastic Beanstalk staging
prefixes (`resources/_runtime/_embedded_extensions/shoc-backend/*` and
`resources/environments/<env-id>/*`) keep the full object and ACL action set. If health, smoke, or the webhook probe fails after that update, the job
restores the previous Elastic Beanstalk version label. Database migrations restores the previous Elastic Beanstalk version label. Database migrations
already applied by the failed bundle are not reverted. Deploy parameters are read from `/shoc-backend/<env>/deploy/*` SSM already applied by the failed bundle are not reverted. Deploy parameters are read from `/shoc-backend/<env>/deploy/*` SSM
parameters this module writes. parameters this module writes.

View file

@ -289,9 +289,32 @@ data "aws_iam_policy_document" "deploy" {
} }
} }
# deploy.yaml uploads each bundle to
# <app>/releases/<environment>/<sha>/<run>/site.zip and Elastic Beanstalk
# reads it back from there. The deploy role never writes another
# environment's release prefix.
dynamic "statement" { dynamic "statement" {
for_each = local.use_legacy_s3_policy ? [] : [1] for_each = local.use_legacy_s3_policy ? [] : [1]
content { content {
sid = "UploadReleaseBundle"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:GetObject",
]
resources = [
"arn:aws:s3:::${local.eb_bucket_name}/${var.eb_application_name}/releases/${var.environment}/*",
]
}
}
# Elastic Beanstalk stages the processed version, embedded extensions,
# and manifests under environment-scoped prefixes and requires object ACLs
# on this BucketOwnerPreferred bucket.
dynamic "statement" {
for_each = local.use_legacy_s3_policy ? [] : [1]
content {
sid = "ManageEnvironmentArtifacts"
effect = "Allow" effect = "Allow"
actions = [ actions = [
"s3:PutObject", "s3:PutObject",
@ -304,7 +327,6 @@ data "aws_iam_policy_document" "deploy" {
"s3:DeleteObject", "s3:DeleteObject",
] ]
resources = [ resources = [
"arn:aws:s3:::${local.eb_bucket_name}/${var.eb_application_name}/*",
"arn:aws:s3:::${local.eb_bucket_name}/resources/_runtime/_embedded_extensions/${var.eb_application_name}/*", "arn:aws:s3:::${local.eb_bucket_name}/resources/_runtime/_embedded_extensions/${var.eb_application_name}/*",
"arn:aws:s3:::${local.eb_bucket_name}/resources/environments/${var.eb_environment_id}/*", "arn:aws:s3:::${local.eb_bucket_name}/resources/environments/${var.eb_environment_id}/*",
] ]

View file

@ -35,7 +35,7 @@ module "environment" {
vpc_id = "vpc-0d16336143f3da25e" vpc_id = "vpc-0d16336143f3da25e"
instance_subnet_ids = ["subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f", "subnet-09eaf2bfa468d206f"] instance_subnet_ids = ["subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f", "subnet-09eaf2bfa468d206f"]
load_balancer_subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"] load_balancer_subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"]
instance_security_group_id = "sg-02ea36a6719217fa2" instance_security_group_id = null
eb_service_role_name = "shoc-eb-service-role" eb_service_role_name = "shoc-eb-service-role"
shared_certificate_arn = "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00" shared_certificate_arn = "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
runtime_role_name = "shoc-backend-staging" runtime_role_name = "shoc-backend-staging"