mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 01:52:10 +00:00
fix(terraform): guard staging import baseline
This commit is contained in:
parent
3b87bf64eb
commit
2c7c9b12e0
6 changed files with 189 additions and 16 deletions
|
|
@ -10,8 +10,10 @@ from pathlib import Path
|
||||||
|
|
||||||
from terraform_import_plan_resources import (
|
from terraform_import_plan_resources import (
|
||||||
DEV_IMPORT_BASELINE,
|
DEV_IMPORT_BASELINE,
|
||||||
DEV_IMPORT_IDS,
|
IMPORT_BASELINES,
|
||||||
|
IMPORT_IDS,
|
||||||
REQUIRED_RESOURCES,
|
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:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
|
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"
|
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"}:
|
if actions != {"no-op"}:
|
||||||
violations.append(
|
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)}"
|
f"got {sorted(actions)}"
|
||||||
)
|
)
|
||||||
expected_import_id = DEV_IMPORT_IDS.get(address)
|
expected_import_id = IMPORT_IDS[args.environment].get(address)
|
||||||
actual_import_id = (
|
actual_import_id = (
|
||||||
resource.get("change", {}).get("importing") or {}
|
resource.get("change", {}).get("importing") or {}
|
||||||
).get("id")
|
).get("id")
|
||||||
|
|
@ -167,6 +215,8 @@ def main() -> int:
|
||||||
|
|
||||||
if initial_import and args.environment == "dev":
|
if initial_import and args.environment == "dev":
|
||||||
validate_dev_import_baseline(resources_by_address, violations)
|
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:
|
if violations:
|
||||||
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
|
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())
|
for address, resource in sorted(resources_by_address.items())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if args.environment == "dev" and initial_import:
|
if args.environment in IMPORT_BASELINES and initial_import:
|
||||||
evidence["asserted_live_baseline"] = DEV_IMPORT_BASELINE
|
evidence["asserted_live_baseline"] = IMPORT_BASELINES[args.environment]
|
||||||
args.evidence_out.write_text(
|
args.evidence_out.write_text(
|
||||||
json.dumps(evidence, indent=2, sort_keys=True) + "\n",
|
json.dumps(evidence, indent=2, sort_keys=True) + "\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,54 @@ DEV_IMPORT_BASELINE = {
|
||||||
"evaluate_target_health": True,
|
"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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ from pathlib import Path
|
||||||
|
|
||||||
from terraform_import_plan_resources import (
|
from terraform_import_plan_resources import (
|
||||||
DEV_IMPORT_BASELINE,
|
DEV_IMPORT_BASELINE,
|
||||||
DEV_IMPORT_IDS,
|
IMPORT_IDS,
|
||||||
REQUIRED_RESOURCES,
|
REQUIRED_RESOURCES,
|
||||||
|
STAGING_IMPORT_BASELINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
|
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
|
||||||
|
|
@ -36,29 +37,40 @@ def run_case(
|
||||||
continue
|
continue
|
||||||
actions = (actions_by_address or {}).get(address, ["no-op"])
|
actions = (actions_by_address or {}).get(address, ["no-op"])
|
||||||
change: dict[str, object] = {"actions": actions}
|
change: dict[str, object] = {"actions": actions}
|
||||||
if environment == "dev":
|
if environment in IMPORT_IDS:
|
||||||
change["importing"] = {
|
change["importing"] = {
|
||||||
"id": (import_id_overrides or {}).get(
|
"id": (import_id_overrides or {}).get(
|
||||||
address, DEV_IMPORT_IDS[address]
|
address, IMPORT_IDS[environment][address]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if address == (
|
||||||
address
|
"module.environment.aws_elastic_beanstalk_environment.this"
|
||||||
== "module.environment.aws_elastic_beanstalk_environment.this"
|
|
||||||
):
|
):
|
||||||
|
baseline = (
|
||||||
|
DEV_IMPORT_BASELINE
|
||||||
|
if environment == "dev"
|
||||||
|
else STAGING_IMPORT_BASELINE
|
||||||
|
)
|
||||||
state: dict[str, object] = {
|
state: dict[str, object] = {
|
||||||
"tags": DEV_IMPORT_BASELINE["environment_tags"],
|
"tags": baseline["environment_tags"],
|
||||||
"setting": [],
|
"setting": [],
|
||||||
}
|
}
|
||||||
change["before"] = state
|
change["before"] = state
|
||||||
change["after"] = state
|
change["after"] = state
|
||||||
elif (
|
elif environment == "dev" and (
|
||||||
address
|
address
|
||||||
== "module.environment.aws_route53_record.api_alias[0]"
|
== "module.environment.aws_route53_record.api_alias[0]"
|
||||||
):
|
):
|
||||||
state = {"alias": [DEV_IMPORT_BASELINE["api_alias"]]}
|
state = {"alias": [DEV_IMPORT_BASELINE["api_alias"]]}
|
||||||
change["before"] = state
|
change["before"] = state
|
||||||
change["after"] = 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 {}):
|
if address in (state_overrides or {}):
|
||||||
change["before"] = (state_overrides or {})[address]
|
change["before"] = (state_overrides or {})[address]
|
||||||
change["after"] = (state_overrides or {})[address]
|
change["after"] = (state_overrides or {})[address]
|
||||||
|
|
@ -177,6 +189,59 @@ def main() -> int:
|
||||||
),
|
),
|
||||||
1,
|
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",
|
"controlled update",
|
||||||
run_case(
|
run_case(
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ resource "aws_route53_record" "api_cname" {
|
||||||
name = var.api_domain
|
name = var.api_domain
|
||||||
type = "CNAME"
|
type = "CNAME"
|
||||||
ttl = 60
|
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 {
|
lifecycle {
|
||||||
prevent_destroy = true
|
prevent_destroy = true
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,12 @@ variable "api_alias_target" {
|
||||||
default = null
|
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" {
|
variable "metadata_before_adoption" {
|
||||||
description = "Exact current metadata preserved while adoption_complete is false."
|
description = "Exact current metadata preserved while adoption_complete is false."
|
||||||
type = object({
|
type = object({
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ module "environment" {
|
||||||
aws_region = local.aws_region
|
aws_region = local.aws_region
|
||||||
environment = "staging"
|
environment = "staging"
|
||||||
adoption_complete = false
|
adoption_complete = false
|
||||||
|
manage_eb_settings = false
|
||||||
eb_application_name = local.eb_application_name
|
eb_application_name = local.eb_application_name
|
||||||
eb_environment_name = local.eb_environment_name
|
eb_environment_name = local.eb_environment_name
|
||||||
eb_environment_id = local.eb_environment_id
|
eb_environment_id = local.eb_environment_id
|
||||||
|
|
@ -59,6 +60,7 @@ module "environment" {
|
||||||
hosted_zone_id = "Z02602739VQWBWCAGXP4"
|
hosted_zone_id = "Z02602739VQWBWCAGXP4"
|
||||||
api_domain = local.api_domain
|
api_domain = local.api_domain
|
||||||
api_record_type = "CNAME"
|
api_record_type = "CNAME"
|
||||||
|
api_cname_target = "awseb--AWSEB-pPXqiRgNnZe8-16996010.us-east-1.elb.amazonaws.com"
|
||||||
metadata_before_adoption = {
|
metadata_before_adoption = {
|
||||||
runtime_role_description = "SHOC backend staging compute role (EB instance profile)"
|
runtime_role_description = "SHOC backend staging compute role (EB instance profile)"
|
||||||
runtime_role_tags = {
|
runtime_role_tags = {
|
||||||
|
|
@ -80,7 +82,6 @@ module "environment" {
|
||||||
Project = "shoc-backend"
|
Project = "shoc-backend"
|
||||||
}
|
}
|
||||||
environment_tags = {
|
environment_tags = {
|
||||||
Name = "shoc-backend-staging"
|
|
||||||
env = "staging"
|
env = "staging"
|
||||||
project = "shoc"
|
project = "shoc"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue