fix(terraform): guard staging import baseline

This commit is contained in:
Adam Moussa 2026-08-31 15:11:04 -04:00
parent 3b87bf64eb
commit 2c7c9b12e0
No known key found for this signature in database
6 changed files with 189 additions and 16 deletions

View file

@ -10,8 +10,10 @@ from pathlib import Path
from terraform_import_plan_resources import (
DEV_IMPORT_BASELINE,
DEV_IMPORT_IDS,
IMPORT_BASELINES,
IMPORT_IDS,
REQUIRED_RESOURCES,
STAGING_IMPORT_BASELINE,
)
@ -92,6 +94,51 @@ def validate_dev_import_baseline(
)
def validate_staging_import_baseline(
resources_by_address: dict[str, dict], violations: list[str]
) -> None:
environment_address = (
"module.environment.aws_elastic_beanstalk_environment.this"
)
route_address = "module.environment.aws_route53_record.api_cname[0]"
expected_tags = STAGING_IMPORT_BASELINE["environment_tags"]
expected_cname = STAGING_IMPORT_BASELINE["api_cname"]
for side in ("before", "after"):
environment = (
resources_by_address.get(environment_address, {})
.get("change", {})
.get(side)
or {}
)
if environment.get("tags") != expected_tags:
violations.append(
f"{environment_address}: {side} environment tags do not match "
f"the exact staging import baseline"
)
if environment.get("setting") != []:
violations.append(
f"{environment_address}: {side} contains managed EB settings "
"during the import-only phase"
)
route = (
resources_by_address.get(route_address, {})
.get("change", {})
.get(side)
or {}
)
actual_cname = {
"records": route.get("records"),
"ttl": route.get("ttl"),
}
if actual_cname != expected_cname:
violations.append(
f"{route_address}: {side} CNAME does not match the exact "
"live ALB target and TTL"
)
def main() -> int:
args = parse_args()
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
@ -143,13 +190,14 @@ def main() -> int:
f"{address}: update is not explicitly allowlisted"
)
if initial_import and args.environment == "dev":
if initial_import and args.environment in IMPORT_IDS:
if actions != {"no-op"}:
violations.append(
f"{address}: initial dev import actions must be ['no-op'], "
f"{address}: initial {args.environment} import actions "
"must be ['no-op'], "
f"got {sorted(actions)}"
)
expected_import_id = DEV_IMPORT_IDS.get(address)
expected_import_id = IMPORT_IDS[args.environment].get(address)
actual_import_id = (
resource.get("change", {}).get("importing") or {}
).get("id")
@ -167,6 +215,8 @@ def main() -> int:
if initial_import and args.environment == "dev":
validate_dev_import_baseline(resources_by_address, violations)
elif initial_import and args.environment == "staging":
validate_staging_import_baseline(resources_by_address, violations)
if violations:
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
@ -191,8 +241,8 @@ def main() -> int:
for address, resource in sorted(resources_by_address.items())
},
}
if args.environment == "dev" and initial_import:
evidence["asserted_live_baseline"] = DEV_IMPORT_BASELINE
if args.environment in IMPORT_BASELINES and initial_import:
evidence["asserted_live_baseline"] = IMPORT_BASELINES[args.environment]
args.evidence_out.write_text(
json.dumps(evidence, indent=2, sort_keys=True) + "\n",
encoding="utf-8",

View file

@ -71,3 +71,54 @@ DEV_IMPORT_BASELINE = {
"evaluate_target_health": True,
},
}
STAGING_IMPORT_IDS = {
"module.environment.aws_elastic_beanstalk_environment.this": "e-6c9m4vb62z",
"module.environment.aws_iam_instance_profile.runtime": "shoc-backend-staging",
"module.environment.aws_iam_role.github_deploy": (
"githubdeploy-shoc-backend-staging"
),
"module.environment.aws_iam_role.runtime": "shoc-backend-staging",
"module.environment.aws_iam_role_policy.github_deploy": (
"githubdeploy-shoc-backend-staging:GithubDeployRoleDefaultPolicyE8F540D1"
),
"module.environment.aws_iam_role_policy.runtime_app_config": (
"shoc-backend-staging:shoc-staging-secrets-read"
),
"module.environment.aws_iam_role_policy.runtime_webhook[0]": (
"shoc-backend-staging:shoc-backend-staging-webhook-secret-access"
),
"module.environment.aws_iam_role_policy_attachment.web_tier": (
"shoc-backend-staging/arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
),
"module.environment.aws_secretsmanager_secret.app_config": (
"arn:aws:secretsmanager:us-east-1:396287094661:secret:"
"shoc/staging/app-config-CVV99L"
),
"module.environment.aws_route53_record.api_cname[0]": (
"Z02602739VQWBWCAGXP4_api.staging.seahaven.com_CNAME"
),
}
STAGING_IMPORT_BASELINE = {
"environment_tags": {
"env": "staging",
"project": "shoc",
},
"api_cname": {
"records": [
"awseb--AWSEB-pPXqiRgNnZe8-16996010.us-east-1.elb.amazonaws.com"
],
"ttl": 60,
},
}
IMPORT_IDS = {
"dev": DEV_IMPORT_IDS,
"staging": STAGING_IMPORT_IDS,
}
IMPORT_BASELINES = {
"dev": DEV_IMPORT_BASELINE,
"staging": STAGING_IMPORT_BASELINE,
}

View file

@ -11,8 +11,9 @@ from pathlib import Path
from terraform_import_plan_resources import (
DEV_IMPORT_BASELINE,
DEV_IMPORT_IDS,
IMPORT_IDS,
REQUIRED_RESOURCES,
STAGING_IMPORT_BASELINE,
)
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
@ -36,29 +37,40 @@ def run_case(
continue
actions = (actions_by_address or {}).get(address, ["no-op"])
change: dict[str, object] = {"actions": actions}
if environment == "dev":
if environment in IMPORT_IDS:
change["importing"] = {
"id": (import_id_overrides or {}).get(
address, DEV_IMPORT_IDS[address]
address, IMPORT_IDS[environment][address]
)
}
if (
address
== "module.environment.aws_elastic_beanstalk_environment.this"
if address == (
"module.environment.aws_elastic_beanstalk_environment.this"
):
baseline = (
DEV_IMPORT_BASELINE
if environment == "dev"
else STAGING_IMPORT_BASELINE
)
state: dict[str, object] = {
"tags": DEV_IMPORT_BASELINE["environment_tags"],
"tags": baseline["environment_tags"],
"setting": [],
}
change["before"] = state
change["after"] = state
elif (
elif environment == "dev" and (
address
== "module.environment.aws_route53_record.api_alias[0]"
):
state = {"alias": [DEV_IMPORT_BASELINE["api_alias"]]}
change["before"] = state
change["after"] = state
elif environment == "staging" and (
address
== "module.environment.aws_route53_record.api_cname[0]"
):
state = STAGING_IMPORT_BASELINE["api_cname"]
change["before"] = state
change["after"] = state
if address in (state_overrides or {}):
change["before"] = (state_overrides or {})[address]
change["after"] = (state_overrides or {})[address]
@ -177,6 +189,59 @@ def main() -> int:
),
1,
),
(
"wrong staging import id",
run_case(
"staging",
import_id_overrides={controlled_address: "wrong-role"},
),
1,
),
(
"wrong staging environment tags",
run_case(
"staging",
state_overrides={
"module.environment.aws_elastic_beanstalk_environment.this": {
"tags": {
"Name": "shoc-backend-staging",
"env": "staging",
"project": "shoc",
},
"setting": [],
}
},
),
1,
),
(
"staging managed settings during import",
run_case(
"staging",
state_overrides={
"module.environment.aws_elastic_beanstalk_environment.this": {
"tags": STAGING_IMPORT_BASELINE["environment_tags"],
"setting": [{"name": "ASPNETCORE_ENVIRONMENT"}],
}
},
),
1,
),
(
"wrong staging cname",
run_case(
"staging",
state_overrides={
"module.environment.aws_route53_record.api_cname[0]": {
"records": [
"shoc-backend-staging.us-east-1.elasticbeanstalk.com"
],
"ttl": 60,
}
},
),
1,
),
(
"controlled update",
run_case(

View file

@ -509,7 +509,7 @@ resource "aws_route53_record" "api_cname" {
name = var.api_domain
type = "CNAME"
ttl = 60
records = [aws_elastic_beanstalk_environment.this.endpoint_url]
records = [var.api_cname_target == null ? aws_elastic_beanstalk_environment.this.endpoint_url : var.api_cname_target]
lifecycle {
prevent_destroy = true

View file

@ -221,6 +221,12 @@ variable "api_alias_target" {
default = null
}
variable "api_cname_target" {
type = string
description = "Exact existing Route 53 CNAME target preserved during import. Null resolves the target from Elastic Beanstalk."
default = null
}
variable "metadata_before_adoption" {
description = "Exact current metadata preserved while adoption_complete is false."
type = object({

View file

@ -27,6 +27,7 @@ module "environment" {
aws_region = local.aws_region
environment = "staging"
adoption_complete = false
manage_eb_settings = false
eb_application_name = local.eb_application_name
eb_environment_name = local.eb_environment_name
eb_environment_id = local.eb_environment_id
@ -59,6 +60,7 @@ module "environment" {
hosted_zone_id = "Z02602739VQWBWCAGXP4"
api_domain = local.api_domain
api_record_type = "CNAME"
api_cname_target = "awseb--AWSEB-pPXqiRgNnZe8-16996010.us-east-1.elb.amazonaws.com"
metadata_before_adoption = {
runtime_role_description = "SHOC backend staging compute role (EB instance profile)"
runtime_role_tags = {
@ -80,7 +82,6 @@ module "environment" {
Project = "shoc-backend"
}
environment_tags = {
Name = "shoc-backend-staging"
env = "staging"
project = "shoc"
}