mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
feat(terraform): complete dev environment adoption (SH-300)
This commit is contained in:
parent
8b5281d357
commit
f532aa6059
7 changed files with 110 additions and 377 deletions
|
|
@ -18,17 +18,11 @@ from terraform_import_plan_resources import (
|
||||||
|
|
||||||
BUCKET_POLICY_ADDRESS = "module.environment_owned.aws_s3_bucket_policy.site"
|
BUCKET_POLICY_ADDRESS = "module.environment_owned.aws_s3_bucket_policy.site"
|
||||||
BUCKET_ADDRESS = "module.environment_owned.aws_s3_bucket.site"
|
BUCKET_ADDRESS = "module.environment_owned.aws_s3_bucket.site"
|
||||||
DEPLOY_POLICY_ADDRESS = (
|
|
||||||
"module.environment_owned.aws_iam_role_policy.github_deploy"
|
|
||||||
)
|
|
||||||
DISTRIBUTION_ADDRESS = (
|
DISTRIBUTION_ADDRESS = (
|
||||||
"module.environment_owned.aws_cloudfront_distribution.site"
|
"module.environment_owned.aws_cloudfront_distribution.site"
|
||||||
)
|
)
|
||||||
ROLE_ADDRESS = "module.environment_owned.aws_iam_role.github_deploy"
|
ROLE_ADDRESS = "module.environment_owned.aws_iam_role.github_deploy"
|
||||||
TAG_UPDATE_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {
|
TAG_UPDATE_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY_ADDRESS}
|
||||||
BUCKET_POLICY_ADDRESS,
|
|
||||||
DEPLOY_POLICY_ADDRESS,
|
|
||||||
}
|
|
||||||
OWNERSHIP_TAGS = {
|
OWNERSHIP_TAGS = {
|
||||||
"Environment": None,
|
"Environment": None,
|
||||||
"ManagedBy": "terraform",
|
"ManagedBy": "terraform",
|
||||||
|
|
@ -255,113 +249,6 @@ def _expected_bucket_policy(environment: str, distribution_id: str) -> dict[str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _expected_pre_adoption_deploy_policy(
|
|
||||||
environment: str,
|
|
||||||
distribution_id: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
config = ENVIRONMENT_CONFIG[environment]
|
|
||||||
bucket_arn = f"arn:aws:s3:::{config['bucket_name']}"
|
|
||||||
distribution_arn = (
|
|
||||||
f"arn:aws:cloudfront::396287094661:distribution/{distribution_id}"
|
|
||||||
)
|
|
||||||
statements: list[dict[str, Any]] = []
|
|
||||||
if environment == "dev":
|
|
||||||
statements.append(
|
|
||||||
{
|
|
||||||
"Sid": "AssumeCdkBootstrapRoles",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": "sts:AssumeRole",
|
|
||||||
"Resource": "arn:aws:iam::396287094661:role/cdk-hnb659fds-*",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
statements.extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Sid": "DescribeStack",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": "cloudformation:DescribeStacks",
|
|
||||||
"Resource": (
|
|
||||||
"arn:aws:cloudformation:us-east-1:396287094661:stack/"
|
|
||||||
f"{config['cloudformation_stack_name']}/*"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:Abort*",
|
|
||||||
"s3:DeleteObject*",
|
|
||||||
"s3:GetBucket*",
|
|
||||||
"s3:GetObject*",
|
|
||||||
"s3:List*",
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:PutObjectLegalHold",
|
|
||||||
"s3:PutObjectRetention",
|
|
||||||
"s3:PutObjectTagging",
|
|
||||||
"s3:PutObjectVersionTagging",
|
|
||||||
],
|
|
||||||
"Resource": [bucket_arn, f"{bucket_arn}/*"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "InvalidateDistribution",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"cloudfront:CreateInvalidation",
|
|
||||||
"cloudfront:GetInvalidation",
|
|
||||||
],
|
|
||||||
"Resource": distribution_arn,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return _canonical({"Version": "2012-10-17", "Statement": statements})
|
|
||||||
|
|
||||||
|
|
||||||
def _expected_deploy_policy(environment: str, distribution_id: str) -> dict[str, Any]:
|
|
||||||
bucket = ENVIRONMENT_CONFIG[environment]["bucket_name"]
|
|
||||||
bucket_arn = f"arn:aws:s3:::{bucket}"
|
|
||||||
distribution_arn = (
|
|
||||||
f"arn:aws:cloudfront::396287094661:distribution/{distribution_id}"
|
|
||||||
)
|
|
||||||
return _canonical(
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Sid": "ReadDeploymentBucket",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:GetBucketLocation",
|
|
||||||
"s3:GetBucketVersioning",
|
|
||||||
"s3:ListBucket",
|
|
||||||
"s3:ListBucketVersions",
|
|
||||||
],
|
|
||||||
"Resource": bucket_arn,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "PublishAndRollbackSiteObjects",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:DeleteObject",
|
|
||||||
"s3:DeleteObjectVersion",
|
|
||||||
"s3:GetObject",
|
|
||||||
"s3:GetObjectVersion",
|
|
||||||
"s3:PutObject",
|
|
||||||
],
|
|
||||||
"Resource": f"{bucket_arn}/*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "InvalidateDistribution",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"cloudfront:CreateInvalidation",
|
|
||||||
"cloudfront:GetInvalidation",
|
|
||||||
],
|
|
||||||
"Resource": distribution_arn,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_tag_update(
|
def _validate_tag_update(
|
||||||
address: str,
|
address: str,
|
||||||
before: dict[str, Any],
|
before: dict[str, Any],
|
||||||
|
|
@ -432,16 +319,10 @@ def _validate_policy_update(
|
||||||
f"{address}: cannot verify policy without the pinned distribution ID"
|
f"{address}: cannot verify policy without the pinned distribution ID"
|
||||||
)
|
)
|
||||||
return violations
|
return violations
|
||||||
expected_before = (
|
expected_before = _expected_pre_adoption_bucket_policy(
|
||||||
_expected_pre_adoption_bucket_policy(environment, distribution_id)
|
environment, distribution_id
|
||||||
if address == BUCKET_POLICY_ADDRESS
|
|
||||||
else _expected_pre_adoption_deploy_policy(environment, distribution_id)
|
|
||||||
)
|
|
||||||
expected_after = (
|
|
||||||
_expected_bucket_policy(environment, distribution_id)
|
|
||||||
if address == BUCKET_POLICY_ADDRESS
|
|
||||||
else _expected_deploy_policy(environment, distribution_id)
|
|
||||||
)
|
)
|
||||||
|
expected_after = _expected_bucket_policy(environment, distribution_id)
|
||||||
if before_policy is not None and before_policy != expected_before:
|
if before_policy is not None and before_policy != expected_before:
|
||||||
violations.append(f"{address}: pre-adoption policy semantics are not exact")
|
violations.append(f"{address}: pre-adoption policy semantics are not exact")
|
||||||
if after_policy is not None and after_policy != expected_after:
|
if after_policy is not None and after_policy != expected_after:
|
||||||
|
|
@ -467,7 +348,7 @@ def _validate_controlled_update(
|
||||||
return [*violations, f"{address}: controlled update requires before/after objects"]
|
return [*violations, f"{address}: controlled update requires before/after objects"]
|
||||||
if address in TAG_UPDATE_ADDRESSES:
|
if address in TAG_UPDATE_ADDRESSES:
|
||||||
violations.extend(_validate_tag_update(address, before, after, environment))
|
violations.extend(_validate_tag_update(address, before, after, environment))
|
||||||
elif address in {BUCKET_POLICY_ADDRESS, DEPLOY_POLICY_ADDRESS}:
|
elif address == BUCKET_POLICY_ADDRESS:
|
||||||
violations.extend(
|
violations.extend(
|
||||||
_validate_policy_update(
|
_validate_policy_update(
|
||||||
address,
|
address,
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ CONTROLLED_UPDATE_ADDRESSES = frozenset(
|
||||||
"module.environment_owned.aws_cloudfront_distribution.site",
|
"module.environment_owned.aws_cloudfront_distribution.site",
|
||||||
"module.environment_owned.aws_cloudfront_function.spa_rewrite",
|
"module.environment_owned.aws_cloudfront_function.spa_rewrite",
|
||||||
"module.environment_owned.aws_iam_role.github_deploy",
|
"module.environment_owned.aws_iam_role.github_deploy",
|
||||||
"module.environment_owned.aws_iam_role_policy.github_deploy",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ BUCKET = "module.environment_owned.aws_s3_bucket.site"
|
||||||
DEPLOY_POLICY = "module.environment_owned.aws_iam_role_policy.github_deploy"
|
DEPLOY_POLICY = "module.environment_owned.aws_iam_role_policy.github_deploy"
|
||||||
ROLE = "module.environment_owned.aws_iam_role.github_deploy"
|
ROLE = "module.environment_owned.aws_iam_role.github_deploy"
|
||||||
DISTRIBUTION = "module.environment_owned.aws_cloudfront_distribution.site"
|
DISTRIBUTION = "module.environment_owned.aws_cloudfront_distribution.site"
|
||||||
TAG_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY, DEPLOY_POLICY}
|
TAG_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY}
|
||||||
|
|
||||||
|
|
||||||
def import_id(environment: str, address: str) -> str:
|
def import_id(environment: str, address: str) -> str:
|
||||||
|
|
@ -111,110 +111,6 @@ def bucket_policy(environment: str) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def pre_adoption_deploy_policy(environment: str) -> dict[str, Any]:
|
|
||||||
config = ENVIRONMENT_CONFIG[environment]
|
|
||||||
bucket_arn = f"arn:aws:s3:::{config['bucket_name']}"
|
|
||||||
distribution_arn = (
|
|
||||||
"arn:aws:cloudfront::396287094661:distribution/"
|
|
||||||
f"{distribution_id(environment)}"
|
|
||||||
)
|
|
||||||
statements: list[dict[str, Any]] = []
|
|
||||||
if environment == "dev":
|
|
||||||
statements.append(
|
|
||||||
{
|
|
||||||
"Sid": "AssumeCdkBootstrapRoles",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": "sts:AssumeRole",
|
|
||||||
"Resource": "arn:aws:iam::396287094661:role/cdk-hnb659fds-*",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
statements.extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Sid": "DescribeStack",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": "cloudformation:DescribeStacks",
|
|
||||||
"Resource": (
|
|
||||||
"arn:aws:cloudformation:us-east-1:396287094661:stack/"
|
|
||||||
f"{config['cloudformation_stack_name']}/*"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:Abort*",
|
|
||||||
"s3:DeleteObject*",
|
|
||||||
"s3:GetBucket*",
|
|
||||||
"s3:GetObject*",
|
|
||||||
"s3:List*",
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:PutObjectLegalHold",
|
|
||||||
"s3:PutObjectRetention",
|
|
||||||
"s3:PutObjectTagging",
|
|
||||||
"s3:PutObjectVersionTagging",
|
|
||||||
],
|
|
||||||
"Resource": [bucket_arn, f"{bucket_arn}/*"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "InvalidateDistribution",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"cloudfront:CreateInvalidation",
|
|
||||||
"cloudfront:GetInvalidation",
|
|
||||||
],
|
|
||||||
"Resource": distribution_arn,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return {"Version": "2012-10-17", "Statement": statements}
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_policy(environment: str) -> dict[str, Any]:
|
|
||||||
bucket = ENVIRONMENT_CONFIG[environment]["bucket_name"]
|
|
||||||
bucket_arn = f"arn:aws:s3:::{bucket}"
|
|
||||||
distribution_arn = (
|
|
||||||
"arn:aws:cloudfront::396287094661:distribution/"
|
|
||||||
f"{distribution_id(environment)}"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Sid": "ReadDeploymentBucket",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:GetBucketLocation",
|
|
||||||
"s3:GetBucketVersioning",
|
|
||||||
"s3:ListBucket",
|
|
||||||
"s3:ListBucketVersions",
|
|
||||||
],
|
|
||||||
"Resource": bucket_arn,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "PublishAndRollbackSiteObjects",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:DeleteObject",
|
|
||||||
"s3:DeleteObjectVersion",
|
|
||||||
"s3:GetObject",
|
|
||||||
"s3:GetObjectVersion",
|
|
||||||
"s3:PutObject",
|
|
||||||
],
|
|
||||||
"Resource": f"{bucket_arn}/*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "InvalidateDistribution",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"cloudfront:CreateInvalidation",
|
|
||||||
"cloudfront:GetInvalidation",
|
|
||||||
],
|
|
||||||
"Resource": distribution_arn,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def tag_change(environment: str, address: str) -> dict[str, Any]:
|
def tag_change(environment: str, address: str) -> dict[str, Any]:
|
||||||
manager = {
|
manager = {
|
||||||
"HcpTerraformWorkspace": ENVIRONMENT_CONFIG[environment]["workspace_name"]
|
"HcpTerraformWorkspace": ENVIRONMENT_CONFIG[environment]["workspace_name"]
|
||||||
|
|
@ -250,20 +146,12 @@ def tag_change(environment: str, address: str) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def policy_change(environment: str, address: str) -> dict[str, Any]:
|
def policy_change(environment: str, address: str) -> dict[str, Any]:
|
||||||
before_policy = (
|
if address != BUCKET_POLICY:
|
||||||
pre_adoption_bucket_policy(environment)
|
raise AssertionError(f"{address} is not a reviewed policy update")
|
||||||
if address == BUCKET_POLICY
|
|
||||||
else pre_adoption_deploy_policy(environment)
|
|
||||||
)
|
|
||||||
after_policy = (
|
|
||||||
bucket_policy(environment)
|
|
||||||
if address == BUCKET_POLICY
|
|
||||||
else deploy_policy(environment)
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
"actions": ["update"],
|
"actions": ["update"],
|
||||||
"before": {"policy": json.dumps(before_policy)},
|
"before": {"policy": json.dumps(pre_adoption_bucket_policy(environment))},
|
||||||
"after": {"policy": json.dumps(after_policy)},
|
"after": {"policy": json.dumps(bucket_policy(environment))},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -395,9 +283,9 @@ class ImportPlanCheckerTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(["dev"], live_roots)
|
self.assertEqual(["dev"], live_roots)
|
||||||
|
|
||||||
def test_dev_root_pins_import_phase_in_code(self) -> None:
|
def test_dev_root_pins_adoption_complete_in_code(self) -> None:
|
||||||
source = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8")
|
source = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8")
|
||||||
self.assertRegex(source, r"\n\s+adoption_complete\s+= false\n")
|
self.assertRegex(source, r"\n\s+adoption_complete\s+= true\n")
|
||||||
self.assertRegex(source, r"adoption_complete\s+= local\.adoption_complete")
|
self.assertRegex(source, r"adoption_complete\s+= local\.adoption_complete")
|
||||||
self.assertNotIn('variable "adoption_complete"', source)
|
self.assertNotIn('variable "adoption_complete"', source)
|
||||||
for root_file in ("main.tf", "imports.tf", "outputs.tf", "providers.tf", "versions.tf"):
|
for root_file in ("main.tf", "imports.tf", "outputs.tf", "providers.tf", "versions.tf"):
|
||||||
|
|
@ -564,54 +452,53 @@ class ImportPlanCheckerTests(unittest.TestCase):
|
||||||
with self.subTest(mutation=mutation):
|
with self.subTest(mutation=mutation):
|
||||||
self.assert_fails(plan, "dev", BUCKET_POLICY)
|
self.assert_fails(plan, "dev", BUCKET_POLICY)
|
||||||
|
|
||||||
def test_deploy_policy_rejects_resource_action_and_extra_statement(self) -> None:
|
def test_github_deploy_policy_stays_byte_identical(self) -> None:
|
||||||
for mutation in ("resource", "action", "extra"):
|
source = (
|
||||||
plan = make_plan(
|
REPOSITORY / "terraform/live/modules/environment-owned/main.tf"
|
||||||
"staging",
|
).read_text(encoding="utf-8")
|
||||||
mode="controlled",
|
document = source.split('data "aws_iam_policy_document" "github_deploy" {', 1)[1]
|
||||||
controlled_updates={DEPLOY_POLICY},
|
document = document.split("resource ", 1)[0]
|
||||||
)
|
self.assertNotIn("var.adoption_complete", document)
|
||||||
policy = copy.deepcopy(deploy_policy("staging"))
|
self.assertIn("AssumeCdkBootstrapRoles", document)
|
||||||
if mutation == "resource":
|
self.assertIn("DescribeStack", document)
|
||||||
policy["Statement"][0]["Resource"] = "*"
|
self.assertNotIn("ReadDeploymentBucket", document)
|
||||||
elif mutation == "action":
|
self.assertNotIn("PublishAndRollbackSiteObjects", document)
|
||||||
policy["Statement"][0]["Action"].append("iam:PassRole")
|
self.assertNotIn(
|
||||||
else:
|
"module.environment_owned.aws_iam_role_policy.github_deploy",
|
||||||
policy["Statement"].append(
|
CONTROLLED_UPDATE_ADDRESSES,
|
||||||
{
|
)
|
||||||
"Sid": "Extra",
|
|
||||||
"Effect": "Allow",
|
def test_deploy_policy_is_not_eligible_for_controlled_update(self) -> None:
|
||||||
"Action": "s3:*",
|
plan = make_plan("dev", mode="controlled", controlled_updates=set())
|
||||||
"Resource": "*",
|
self.assert_fails(plan, "dev", DEPLOY_POLICY)
|
||||||
}
|
plan = make_plan("dev", mode="controlled", controlled_updates=set())
|
||||||
)
|
resource(plan, DEPLOY_POLICY)["change"] = {
|
||||||
resource(plan, DEPLOY_POLICY)["change"]["after"]["policy"] = json.dumps(
|
"actions": ["update"],
|
||||||
policy
|
"before": {"policy": "{}"},
|
||||||
)
|
"after": {"policy": '{"Version":"2012-10-17"}'},
|
||||||
with self.subTest(mutation=mutation):
|
}
|
||||||
self.assert_fails(plan, "staging", DEPLOY_POLICY)
|
self.assert_fails(plan, "dev", DEPLOY_POLICY)
|
||||||
|
|
||||||
def test_policy_updates_require_exact_pre_adoption_state(self) -> None:
|
def test_policy_updates_require_exact_pre_adoption_state(self) -> None:
|
||||||
for environment in REQUIRED_RESOURCES:
|
for environment in REQUIRED_RESOURCES:
|
||||||
for address in (BUCKET_POLICY, DEPLOY_POLICY):
|
plan = make_plan(
|
||||||
plan = make_plan(
|
environment,
|
||||||
environment,
|
mode="controlled",
|
||||||
mode="controlled",
|
controlled_updates={BUCKET_POLICY},
|
||||||
controlled_updates={address},
|
)
|
||||||
)
|
change = resource(plan, BUCKET_POLICY)["change"]
|
||||||
change = resource(plan, address)["change"]
|
before = json.loads(change["before"]["policy"])
|
||||||
before = json.loads(change["before"]["policy"])
|
before["Statement"].append(
|
||||||
before["Statement"].append(
|
{
|
||||||
{
|
"Sid": "UnexpectedDrift",
|
||||||
"Sid": "UnexpectedDrift",
|
"Effect": "Deny",
|
||||||
"Effect": "Deny",
|
"Action": "*",
|
||||||
"Action": "*",
|
"Resource": "*",
|
||||||
"Resource": "*",
|
}
|
||||||
}
|
)
|
||||||
)
|
change["before"]["policy"] = json.dumps(before)
|
||||||
change["before"]["policy"] = json.dumps(before)
|
with self.subTest(environment=environment):
|
||||||
with self.subTest(environment=environment, address=address):
|
self.assert_fails(plan, environment, BUCKET_POLICY)
|
||||||
self.assert_fails(plan, environment, address)
|
|
||||||
|
|
||||||
def test_controlled_update_rejects_unknown_and_replace_paths(self) -> None:
|
def test_controlled_update_rejects_unknown_and_replace_paths(self) -> None:
|
||||||
for field, value in (
|
for field, value in (
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ This tree adopts the existing Sea Haven SHOC frontend dev hosting resources
|
||||||
into HCP Terraform without recreating them. It mirrors the backend adoption
|
into HCP Terraform without recreating them. It mirrors the backend adoption
|
||||||
(`shoc-backend` #94, #98, #99, #102) and lands in three PRs:
|
(`shoc-backend` #94, #98, #99, #102) and lands in three PRs:
|
||||||
|
|
||||||
| PR | Branch | Change |
|
| PR | Branch | Change |
|
||||||
| --- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
| --- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| A | `feature/frontend-terraform-adoption` | This PR. Dev root with `adoption_complete = false`, import guard, CDK retain mode, push-to-`dev` deploy off. |
|
| A | `feature/frontend-terraform-adoption` | Merged. Dev root with `adoption_complete = false`, import guard, CDK retain mode, push-to-`dev` deploy off. |
|
||||||
| B | `feature/terraform-dev-adoption` | `adoption_complete = true`: ownership tags, bucket policy drops the auto-delete grant, CloudFormation detaches. |
|
| B | `feature/terraform-dev-adoption` | This PR. `adoption_complete = true`: ownership tags, bucket policy drops the auto-delete grant. |
|
||||||
| C | `feature/terraform-dev-content-cd` | Content CD through Terraform: release prefixes, pointer object, origin group, invalidation action, rollback. |
|
| C | `feature/terraform-dev-content-cd` | Content CD through Terraform: release prefixes, pointer object, origin group, invalidation action, rollback. |
|
||||||
|
|
||||||
Creating these files, formatting them, initializing with `-backend=false`, and
|
Creating these files, formatting them, initializing with `-backend=false`, and
|
||||||
validating them does not authorize an AWS, HCP Terraform, GitHub,
|
validating them does not authorize an AWS, HCP Terraform, GitHub,
|
||||||
|
|
@ -45,9 +45,8 @@ before the first release after any Terraform merge:
|
||||||
`>= 1.9.0, < 2.0.0`; CI validates with `1.16.0`).
|
`>= 1.9.0, < 2.0.0`; CI validates with `1.16.0`).
|
||||||
- Dynamic AWS credentials only: environment variables
|
- Dynamic AWS credentials only: environment variables
|
||||||
`TFC_AWS_PROVIDER_AUTH=true`, `TFC_AWS_PLAN_ROLE_ARN`, and
|
`TFC_AWS_PROVIDER_AUTH=true`, `TFC_AWS_PLAN_ROLE_ARN`, and
|
||||||
`TFC_AWS_APPLY_ROLE_ARN` pointing at the `seahaven-org-baseline` roles
|
`TFC_AWS_APPLY_ROLE_ARN` pointing at `hcptf-shoc-frontend-new-dev-plan`
|
||||||
`hcptf-shoc-frontend-new-dev-plan` and `hcptf-shoc-frontend-new-dev`. No
|
and `hcptf-shoc-frontend-new-dev`. No access keys.
|
||||||
access keys.
|
|
||||||
- **No** `adoption_complete` workspace variable. The dev root pins it in code
|
- **No** `adoption_complete` workspace variable. The dev root pins it in code
|
||||||
(`local.adoption_complete`) so the value under review is the value that
|
(`local.adoption_complete`) so the value under review is the value that
|
||||||
applies. `scripts/test-terraform-import-plan-check.py` fails if a `variable`
|
applies. `scripts/test-terraform-import-plan-check.py` fails if a `variable`
|
||||||
|
|
@ -86,7 +85,6 @@ The following remain outside state:
|
||||||
- `CDKToolkit` resources and CDK metadata
|
- `CDKToolkit` resources and CDK metadata
|
||||||
- the S3 auto-delete custom resource, its provider Lambda and role
|
- the S3 auto-delete custom resource, its provider Lambda and role
|
||||||
- the HCP plan/apply roles and the deploy-role permissions boundary
|
- the HCP plan/apply roles and the deploy-role permissions boundary
|
||||||
(`seahaven-org-baseline` owns them)
|
|
||||||
|
|
||||||
## Exact live inventory (dev)
|
## Exact live inventory (dev)
|
||||||
|
|
||||||
|
|
@ -130,7 +128,7 @@ If read-back after that deploy differs from the root in any other way, update
|
||||||
the root to the observed value and prove a zero-change import plan. Do not
|
the root to the observed value and prove a zero-change import plan. Do not
|
||||||
approve drift through the controlled-update checker.
|
approve drift through the controlled-update checker.
|
||||||
|
|
||||||
## Phase 1: import-first adoption (this PR)
|
## Phase 1: import-first adoption (merged)
|
||||||
|
|
||||||
Each step is gated. State the impact, get the go, act, read back, record.
|
Each step is gated. State the impact, get the go, act, read back, record.
|
||||||
|
|
||||||
|
|
@ -177,7 +175,7 @@ Each step is gated. State the impact, get the go, act, read back, record.
|
||||||
After Phase 1 CloudFormation still owns every resource. Terraform holds state
|
After Phase 1 CloudFormation still owns every resource. Terraform holds state
|
||||||
for them and nothing else.
|
for them and nothing else.
|
||||||
|
|
||||||
## Phase 2: controlled ownership transfer (PR B)
|
## Phase 2: controlled ownership transfer (this PR)
|
||||||
|
|
||||||
PR B pins `adoption_complete = true`. The controlled apply may update only:
|
PR B pins `adoption_complete = true`. The controlled apply may update only:
|
||||||
|
|
||||||
|
|
@ -189,15 +187,19 @@ PR B pins `adoption_complete = true`. The controlled apply may update only:
|
||||||
- `module.environment_owned.aws_iam_role.github_deploy` (tags)
|
- `module.environment_owned.aws_iam_role.github_deploy` (tags)
|
||||||
|
|
||||||
The OAC, both Route 53 records, and the deploy inline policy must be no-op.
|
The OAC, both Route 53 records, and the deploy inline policy must be no-op.
|
||||||
PR B keeps the post-adoption inline policy byte-identical to live so the
|
PR B keeps the GitHub deploy inline policy byte-identical to live so
|
||||||
policy address does not appear in the plan. Run the checker with one
|
`aws_iam_role_policy.github_deploy` does not appear in the plan. Run the
|
||||||
`--allow-update-address` per updating address; it rejects unused allowlist
|
checker with one `--allow-update-address` per updating address; it rejects
|
||||||
entries, unknown values, and replacements.
|
unused allowlist entries, unknown values, and replacements:
|
||||||
|
|
||||||
Dependency: `hcptf-shoc-frontend-new-dev` currently lacks
|
```bash
|
||||||
`cloudfront:UpdateDistribution` and `cloudfront:UpdateFunction`. Codify the
|
python3 scripts/check-terraform-import-plan.py plan.json --environment dev \
|
||||||
expansion in `seahaven-org-baseline` (cross-family plus security review) and
|
--allow-update-address module.environment_owned.aws_s3_bucket.site \
|
||||||
deploy it before the controlled apply.
|
--allow-update-address module.environment_owned.aws_s3_bucket_policy.site \
|
||||||
|
--allow-update-address module.environment_owned.aws_cloudfront_distribution.site \
|
||||||
|
--allow-update-address module.environment_owned.aws_cloudfront_function.spa_rewrite \
|
||||||
|
--allow-update-address module.environment_owned.aws_iam_role.github_deploy
|
||||||
|
```
|
||||||
|
|
||||||
After the apply and a no-op plan, deploy the same reviewed CDK SHA with
|
After the apply and a no-op plan, deploy the same reviewed CDK SHA with
|
||||||
`--parameters ManageSiteInfrastructure=false`. Expect `DELETE_SKIPPED` on the
|
`--parameters ManageSiteInfrastructure=false`. Expect `DELETE_SKIPPED` on the
|
||||||
|
|
@ -205,6 +207,9 @@ After the apply and a no-op plan, deploy the same reviewed CDK SHA with
|
||||||
`ManageSiteInfrastructure=true` again after that. See
|
`ManageSiteInfrastructure=true` again after that. See
|
||||||
[`infra/cdk/README.md`](../infra/cdk/README.md).
|
[`infra/cdk/README.md`](../infra/cdk/README.md).
|
||||||
|
|
||||||
|
Confirm `dev.seahaven.com` still serves and that a manual `workflow_dispatch`
|
||||||
|
of `deploy.yml` can still upload with the unchanged GitHub content policy.
|
||||||
|
|
||||||
## Phase 3: content CD through Terraform (PR C)
|
## Phase 3: content CD through Terraform (PR C)
|
||||||
|
|
||||||
Summary only; PR C carries the full design. GitHub builds and uploads to an
|
Summary only; PR C carries the full design. GitHub builds and uploads to an
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
locals {
|
locals {
|
||||||
# Import-first phase. Pinned in code, never a workspace variable: the
|
# Controlled ownership transfer. Pinned in code, never a workspace variable.
|
||||||
# controlled ownership transfer flips this to true in its own reviewed PR.
|
adoption_complete = true
|
||||||
adoption_complete = false
|
|
||||||
|
|
||||||
environment = "dev"
|
environment = "dev"
|
||||||
workspace_name = "shoc-frontend-new-dev"
|
workspace_name = "shoc-frontend-new-dev"
|
||||||
|
|
|
||||||
|
|
@ -109,8 +109,11 @@ data "aws_iam_policy_document" "github_deploy_assume" {
|
||||||
}
|
}
|
||||||
|
|
||||||
data "aws_iam_policy_document" "github_deploy" {
|
data "aws_iam_policy_document" "github_deploy" {
|
||||||
|
# Byte-identical to the live GitHub content policy through Phase 2 so
|
||||||
|
# aws_iam_role_policy.github_deploy stays no-op. Phase 3 replaces this
|
||||||
|
# with the release-prefix policy.
|
||||||
dynamic "statement" {
|
dynamic "statement" {
|
||||||
for_each = !var.adoption_complete && var.environment == "dev" ? [1] : []
|
for_each = var.environment == "dev" ? [1] : []
|
||||||
|
|
||||||
content {
|
content {
|
||||||
sid = "AssumeCdkBootstrapRoles"
|
sid = "AssumeCdkBootstrapRoles"
|
||||||
|
|
@ -120,72 +123,31 @@ data "aws_iam_policy_document" "github_deploy" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic "statement" {
|
statement {
|
||||||
for_each = var.adoption_complete ? [] : [1]
|
sid = "DescribeStack"
|
||||||
|
effect = "Allow"
|
||||||
content {
|
actions = ["cloudformation:DescribeStacks"]
|
||||||
sid = "DescribeStack"
|
resources = ["arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${var.cloudformation_stack_name}/*"]
|
||||||
effect = "Allow"
|
|
||||||
actions = ["cloudformation:DescribeStacks"]
|
|
||||||
resources = ["arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${var.cloudformation_stack_name}/*"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic "statement" {
|
statement {
|
||||||
for_each = var.adoption_complete ? [] : [1]
|
effect = "Allow"
|
||||||
|
actions = [
|
||||||
content {
|
"s3:Abort*",
|
||||||
effect = "Allow"
|
"s3:DeleteObject*",
|
||||||
actions = [
|
"s3:GetBucket*",
|
||||||
"s3:Abort*",
|
"s3:GetObject*",
|
||||||
"s3:DeleteObject*",
|
"s3:List*",
|
||||||
"s3:GetBucket*",
|
"s3:PutObject",
|
||||||
"s3:GetObject*",
|
"s3:PutObjectLegalHold",
|
||||||
"s3:List*",
|
"s3:PutObjectRetention",
|
||||||
"s3:PutObject",
|
"s3:PutObjectTagging",
|
||||||
"s3:PutObjectLegalHold",
|
"s3:PutObjectVersionTagging",
|
||||||
"s3:PutObjectRetention",
|
]
|
||||||
"s3:PutObjectTagging",
|
resources = [
|
||||||
"s3:PutObjectVersionTagging",
|
local.bucket_arn,
|
||||||
]
|
"${local.bucket_arn}/*",
|
||||||
resources = [
|
]
|
||||||
local.bucket_arn,
|
|
||||||
"${local.bucket_arn}/*",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamic "statement" {
|
|
||||||
for_each = var.adoption_complete ? [1] : []
|
|
||||||
|
|
||||||
content {
|
|
||||||
sid = "ReadDeploymentBucket"
|
|
||||||
effect = "Allow"
|
|
||||||
actions = [
|
|
||||||
"s3:GetBucketLocation",
|
|
||||||
"s3:GetBucketVersioning",
|
|
||||||
"s3:ListBucket",
|
|
||||||
"s3:ListBucketVersions",
|
|
||||||
]
|
|
||||||
resources = [local.bucket_arn]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamic "statement" {
|
|
||||||
for_each = var.adoption_complete ? [1] : []
|
|
||||||
|
|
||||||
content {
|
|
||||||
sid = "PublishAndRollbackSiteObjects"
|
|
||||||
effect = "Allow"
|
|
||||||
actions = [
|
|
||||||
"s3:DeleteObject",
|
|
||||||
"s3:DeleteObjectVersion",
|
|
||||||
"s3:GetObject",
|
|
||||||
"s3:GetObjectVersion",
|
|
||||||
"s3:PutObject",
|
|
||||||
]
|
|
||||||
resources = ["${local.bucket_arn}/*"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statement {
|
statement {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ variable "environment" {
|
||||||
|
|
||||||
variable "adoption_complete" {
|
variable "adoption_complete" {
|
||||||
type = bool
|
type = bool
|
||||||
description = "Switches only ownership tags and the deploy policy to their adopted values."
|
description = "Switches ownership tags and drops the auto-delete helper grant from the bucket policy. The GitHub deploy inline policy stays byte-identical to live until the content-CD PR."
|
||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue