refactor(terraform): keep live/dev and live/staging as HCP roots

Leave the adopted working directories in place so this CD PR does not
retarget two live HCP workspaces. Flattening stays a later change.
This commit is contained in:
Adam Moussa 2026-09-17 17:13:32 -04:00
parent 6ae9766eca
commit 53c1acb19f
No known key found for this signature in database
30 changed files with 447 additions and 241 deletions

View file

@ -39,13 +39,15 @@ jobs:
- name: Terraform fmt - name: Terraform fmt
run: terraform fmt -check -recursive terraform run: terraform fmt -check -recursive terraform
- name: Terraform init - name: Validate live/dev
run: terraform -chdir=terraform init -backend=false -input=false -lockfile=readonly -no-color run: |
terraform -chdir=terraform/live/dev init -backend=false -input=false -lockfile=readonly -no-color
terraform -chdir=terraform/live/dev validate -no-color
- name: Terraform validate - name: Validate live/staging
run: terraform -chdir=terraform validate -no-color run: |
env: terraform -chdir=terraform/live/staging init -backend=false -input=false -lockfile=readonly -no-color
TF_VAR_environment: dev terraform -chdir=terraform/live/staging validate -no-color
- name: Import plan guard tests - name: Import plan guard tests
run: python3 scripts/test-terraform-import-plan-check.py run: python3 scripts/test-terraform-import-plan-check.py

View file

@ -14,24 +14,24 @@ isolation). A task is not done until this is green.
## Gate matrix ## Gate matrix
| Gate | Command / rule source | Enforced by | Scope | | Gate | Command / rule source | Enforced by | Scope |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------------- | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------- |
| Formatting | `npm run format:check` (Prettier) | `verify` + lint-staged | Whole repo | | Formatting | `npm run format:check` (Prettier) | `verify` + lint-staged | Whole repo |
| Lint, zero warnings | `npm run lint` → `eslint . --max-warnings=0` | `verify` + CI | Governed TS/TSX (`eslint.config.js`) | | Lint, zero warnings | `npm run lint` → `eslint . --max-warnings=0` | `verify` + CI | Governed TS/TSX (`eslint.config.js`) |
| Type-check + production build | `npm run build` → `tsc -b && vite build` | `verify` + CI | Whole app | | Type-check + production build | `npm run build` → `tsc -b && vite build` | `verify` + CI | Whole app |
| Unit tests | `npm test` → `vitest run` | `verify` + CI | `src/test/**`, `config/**/*.test.ts` | | Unit tests | `npm test` → `vitest run` | `verify` + CI | `src/test/**`, `config/**/*.test.ts` |
| Conditional rendering (no `: null`) | `no-restricted-syntax` in `eslint.config.js` | lint | Governed TSX | | Conditional rendering (no `: null`) | `no-restricted-syntax` in `eslint.config.js` | lint | Governed TSX |
| Boolean-only JSX `&&` | `seahaven/no-non-boolean-jsx-and` (type-aware) in `eslint-rules/` | lint | Governed TSX | | Boolean-only JSX `&&` | `seahaven/no-non-boolean-jsx-and` (type-aware) in `eslint-rules/` | lint | Governed TSX |
| Shared `Text` typography | `no-restricted-syntax` (raw `p`/`h1`–`h6`) + `seahaven/no-vp-error-outside-text` | lint | Governed TSX | | Shared `Text` typography | `no-restricted-syntax` (raw `p`/`h1`–`h6`) + `seahaven/no-vp-error-outside-text` | lint | Governed TSX |
| Hooks correctness | `eslint-plugin-react-hooks` recommended (incl. `exhaustive-deps`) under zero-warnings | lint | Governed TS/TSX | | Hooks correctness | `eslint-plugin-react-hooks` recommended (incl. `exhaustive-deps`) under zero-warnings | lint | Governed TS/TSX |
| Godfile ratchet (file length) | `scripts/governance-check.mjs` + `scripts/governance-baseline.json` | `governance` | `src/**`, `config/**` (non-test) | | Godfile ratchet (file length) | `scripts/governance-check.mjs` + `scripts/governance-baseline.json` | `governance` | `src/**`, `config/**` (non-test) |
| Changed-file maintainability | `scripts/governance-check.mjs` → ESLint (`complexity`, `max-lines-per-function`, `max-params`, `max-depth`) | `governance` | Changed TS/TSX vs base ref | | Changed-file maintainability | `scripts/governance-check.mjs` → ESLint (`complexity`, `max-lines-per-function`, `max-params`, `max-depth`) | `governance` | Changed TS/TSX vs base ref |
| Terraform import-plan contract | `npm run test:terraform-import-plan` → `scripts/test-terraform-import-plan-check.py` | `governance` + CI | Synthetic plan JSON + canonical maps | | Terraform import-plan contract | `npm run test:terraform-import-plan` → `scripts/test-terraform-import-plan-check.py` | `governance` + CI | Synthetic plan JSON + canonical maps |
| Terraform formatting/validation | `npm run test:terraform` → `scripts/terraform-validate.mjs` | `governance` + CI | `terraform/` | | Terraform formatting/validation | `npm run test:terraform` → `scripts/terraform-validate.mjs` | `governance` + CI | `terraform/live/dev`, `terraform/live/staging` |
| HCP run guard | `npm run test:hcp-run-guard` → `scripts/test-hcp-run-guard.py` | `governance` + CI | Workspace invariants + apply reconcile | | HCP run guard | `npm run test:hcp-run-guard` → `scripts/test-hcp-run-guard.py` | `governance` + CI | Workspace invariants + apply reconcile |
| CloudFront release verify | `npm run test:cloudfront-release-verify` → `scripts/test-verify-cloudfront-release.sh` | `governance` + CI | Stubbed aws/curl | | CloudFront release verify | `npm run test:cloudfront-release-verify` → `scripts/test-verify-cloudfront-release.sh` | `governance` + CI | Stubbed aws/curl |
| GitHub workflow shell | `npm run test:github-workflows` → `scripts/check-github-workflows.sh` | `governance` + CI | `bash -n` + actionlint | | GitHub workflow shell | `npm run test:github-workflows` → `scripts/check-github-workflows.sh` | `governance` + CI | `bash -n` + actionlint |
| G13 App/Terraform isolation | `python3 scripts/check_app_terraform_isolation.py` vs `GOVERNANCE_BASE` | `governance` + CI | Deployable app files vs `terraform/` | | G13 App/Terraform isolation | `python3 scripts/check_app_terraform_isolation.py` vs `GOVERNANCE_BASE` | `governance` + CI | Deployable app files vs `terraform/` |
## No-false-pass guarantees ## No-false-pass guarantees

View file

@ -34,9 +34,9 @@ graph LR
TF[HCP Terraform] -->|bucket CloudFront IAM SSM| CF TF[HCP Terraform] -->|bucket CloudFront IAM SSM| CF
``` ```
Dev and staging hosting live in one `terraform/` tree. GitHub Dev and staging hosting live in `terraform/live/dev` and
`.github/workflows/deploy-web.yaml` syncs content. The older pointer CD `terraform/live/staging`. GitHub `.github/workflows/deploy-web.yaml` syncs
(`deploy.yml`) stays in the tree until cutover. content. The older pointer CD (`deploy.yml`) stays in the tree until cutover.
Frontend stack: React 19, TypeScript, Vite, Tailwind CSS 4 + MUI, TanStack Frontend stack: React 19, TypeScript, Vite, Tailwind CSS 4 + MUI, TanStack
Query, React Router (via `@generouted/react-router`), React Hook Form + Zod, Query, React Router (via `@generouted/react-router`), React Hook Form + Zod,
@ -46,7 +46,7 @@ architecture plan for the keep/discard migration matrix).
## AWS Resources ## AWS Resources
HCP workspace **`shoc-frontend-new-dev`** — account `396287094661`, region HCP workspace **`shoc-frontend-new-dev`** — account `396287094661`, region
`us-east-1`. Defined in [`terraform/`](terraform/). `us-east-1`. Defined in [`terraform/live/dev`](terraform/live/dev).
| Resource | Name | Purpose | | Resource | Name | Purpose |
| ----------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | ----------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
@ -84,7 +84,7 @@ build otherwise. See [`.env.example`](.env.example),
[`.env.development`](.env.development), and [`.env.production`](.env.production). [`.env.development`](.env.development), and [`.env.production`](.env.production).
Pinned hosting constants (domain, certificate ARN, hosted zone) live in Pinned hosting constants (domain, certificate ARN, hosted zone) live in
[`terraform/locals.tf`](terraform/locals.tf). [`terraform/live/dev/main.tf`](terraform/live/dev/main.tf).
## Local Development ## Local Development

View file

@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const TERRAFORM = process.env.TERRAFORM_BIN || "terraform"; const TERRAFORM = process.env.TERRAFORM_BIN || "terraform";
const TERRAFORM_ROOT = path.join(ROOT, "terraform"); const LIVE_ROOTS = ["terraform/live/dev", "terraform/live/staging"];
function run(args, cwd = TERRAFORM_ROOT, env = process.env) { function run(args, cwd = ROOT, env = process.env) {
const result = spawnSync(TERRAFORM, args, { const result = spawnSync(TERRAFORM, args, {
cwd, cwd,
encoding: "utf8", encoding: "utf8",
@ -24,17 +24,11 @@ function run(args, cwd = TERRAFORM_ROOT, env = process.env) {
} }
} }
run(["fmt", "-check", "-recursive", TERRAFORM_ROOT], ROOT); run(["fmt", "-check", "-recursive", "terraform"]);
// -backend=false never touches HCP state; -lockfile=readonly refuses to for (const liveRoot of LIVE_ROOTS) {
// silently rewrite the committed provider lock. const abs = path.join(ROOT, liveRoot);
run(["init", "-backend=false", "-input=false", "-lockfile=readonly", "-no-color"]); run(["init", "-backend=false", "-input=false", "-lockfile=readonly", "-no-color"], abs);
run(["validate", "-no-color"], TERRAFORM_ROOT, { run(["validate", "-no-color"], abs);
...process.env, }
TF_VAR_environment: "dev",
});
run(["validate", "-no-color"], TERRAFORM_ROOT, {
...process.env,
TF_VAR_environment: "staging",
});
console.log("Terraform formatting and validation passed for terraform/."); console.log("Terraform formatting and validation passed for terraform/live/dev and terraform/live/staging.");

View file

@ -1,7 +1,7 @@
"""Canonical frontend Terraform ownership and import-ID maps. """Canonical frontend Terraform ownership and import-ID maps.
Dev is already in HCP state. Staging constants authorize the first import of Dev is already in HCP state. Staging constants authorize the first import of
the live CDK stack onto the same terraform/ root. the live CDK stack onto terraform/live/staging.
""" """
COMMON_RESOURCES = { COMMON_RESOURCES = {

View file

@ -279,7 +279,7 @@ class ImportPlanCheckerTests(unittest.TestCase):
def test_cloudfront_function_source_matches_exact_nine_line_join(self) -> None: def test_cloudfront_function_source_matches_exact_nine_line_join(self) -> None:
source = ( source = (
REPOSITORY / "terraform/modules/environment-owned/main.tf" REPOSITORY / "terraform/live/modules/environment-owned/main.tf"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
expected = """ spa_rewrite_code = join("\\n", [ expected = """ spa_rewrite_code = join("\\n", [
"function handler(event) {", "function handler(event) {",
@ -294,31 +294,31 @@ class ImportPlanCheckerTests(unittest.TestCase):
])""" ])"""
self.assertIn(expected, source) self.assertIn(expected, source)
def test_single_terraform_root(self) -> None: def test_live_roots_are_not_flattened(self) -> None:
terraform_root = REPOSITORY / "terraform" live = REPOSITORY / "terraform" / "live"
self.assertTrue((terraform_root / "versions.tf").is_file()) self.assertTrue((live / "dev" / "versions.tf").is_file())
self.assertTrue((terraform_root / "main.tf").is_file()) self.assertTrue((live / "dev" / "main.tf").is_file())
self.assertFalse((terraform_root / "live").exists()) self.assertTrue((live / "staging" / "versions.tf").is_file())
self.assertTrue((live / "staging" / "main.tf").is_file())
self.assertTrue((live / "modules" / "environment-owned" / "main.tf").is_file())
self.assertFalse((REPOSITORY / "terraform" / "versions.tf").exists())
self.assertFalse((REPOSITORY / "terraform" / "main.tf").exists())
def test_dev_adoption_complete_is_pinned_in_locals(self) -> None: def test_dev_adoption_complete_is_pinned_in_locals(self) -> None:
source = (REPOSITORY / "terraform/locals.tf").read_text(encoding="utf-8") dev = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8")
self.assertRegex( staging = (REPOSITORY / "terraform/live/staging/main.tf").read_text(
source, encoding="utf-8"
r"dev = \{[\s\S]*?adoption_complete\s+= true",
) )
self.assertRegex( self.assertRegex(dev, r"adoption_complete\s+= true")
source, self.assertRegex(staging, r"adoption_complete\s+= false")
r"staging = \{[\s\S]*?adoption_complete\s+= false", self.assertNotIn('variable "adoption_complete"', dev)
) self.assertNotIn('variable "environment"', dev)
variables = (REPOSITORY / "terraform/variables.tf").read_text(encoding="utf-8") self.assertNotIn('variable "release_version_label"', dev)
self.assertIn('variable "environment"', variables)
self.assertNotIn('variable "adoption_complete"', variables)
self.assertNotIn('variable "release_version_label"', variables)
def test_managed_modules_use_direct_pinned_inputs(self) -> None: def test_managed_modules_use_direct_pinned_inputs(self) -> None:
source = (REPOSITORY / "terraform/main.tf").read_text(encoding="utf-8") source = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8")
expected = { expected = {
"hosted_zone_id": "local.stack.hosted_zone_id", "hosted_zone_id": "local.hosted_zone_id",
"certificate_arn": "local.certificate_arn", "certificate_arn": "local.certificate_arn",
"github_oidc_provider_arn": "local.github_oidc_arn", "github_oidc_provider_arn": "local.github_oidc_arn",
"cache_policy_id": "local.cache_policy_id", "cache_policy_id": "local.cache_policy_id",
@ -459,7 +459,7 @@ class ImportPlanCheckerTests(unittest.TestCase):
def test_github_deploy_policy_is_bucket_root_sync(self) -> None: def test_github_deploy_policy_is_bucket_root_sync(self) -> None:
source = ( source = (
REPOSITORY / "terraform/modules/environment-owned/main.tf" REPOSITORY / "terraform/live/modules/environment-owned/main.tf"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
document = source.split('data "aws_iam_policy_document" "github_deploy" {', 1)[1] document = source.split('data "aws_iam_policy_document" "github_deploy" {', 1)[1]
document = document.split("resource ", 1)[0] document = document.split("resource ", 1)[0]

View file

@ -13,8 +13,8 @@ class IsolationTests(unittest.TestCase):
self.assertIsNone( self.assertIsNone(
isolation_violation( isolation_violation(
[ [
"terraform/main.tf", "terraform/live/dev/main.tf",
"terraform/README.md", "terraform/live/README.md",
] ]
) )
) )
@ -35,7 +35,7 @@ class IsolationTests(unittest.TestCase):
self.assertIsNone( self.assertIsNone(
isolation_violation( isolation_violation(
[ [
"terraform/modules/environment-owned/main.tf", "terraform/live/modules/environment-owned/main.tf",
".github/workflows/deploy-web.yaml", ".github/workflows/deploy-web.yaml",
"QUALITY_GATES.md", "QUALITY_GATES.md",
"scripts/governance-check.mjs", "scripts/governance-check.mjs",
@ -48,21 +48,21 @@ class IsolationTests(unittest.TestCase):
def test_mixed_src_and_terraform_fails(self) -> None: def test_mixed_src_and_terraform_fails(self) -> None:
violation = isolation_violation( violation = isolation_violation(
[ [
"terraform/main.tf", "terraform/live/dev/main.tf",
"src/app/routes.tsx", "src/app/routes.tsx",
] ]
) )
self.assertIsNotNone(violation) self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], []) terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["terraform/main.tf"]) self.assertEqual(terraform_files, ["terraform/live/dev/main.tf"])
self.assertEqual(app_files, ["src/app/routes.tsx"]) self.assertEqual(app_files, ["src/app/routes.tsx"])
def test_mixed_vite_config_and_terraform_fails(self) -> None: def test_mixed_vite_config_and_terraform_fails(self) -> None:
violation = isolation_violation(["terraform/variables.tf", "vite.config.ts"]) violation = isolation_violation(["terraform/live/dev/versions.tf", "vite.config.ts"])
self.assertIsNotNone(violation) self.assertIsNotNone(violation)
def test_mixed_env_and_terraform_fails(self) -> None: def test_mixed_env_and_terraform_fails(self) -> None:
violation = isolation_violation(["terraform/locals.tf", ".env.production"]) violation = isolation_violation(["terraform/live/dev/main.tf", ".env.production"])
self.assertIsNotNone(violation) self.assertIsNotNone(violation)

View file

@ -1,6 +1,6 @@
# Frontend Terraform (SPA CD) # Frontend Terraform (SPA CD)
One `terraform/` root for `dev` and `staging` in AWS account `396287094661`. `terraform/live/dev` and `terraform/live/staging` in AWS account `396287094661`.
HCP Terraform owns the bucket, CloudFront, DNS, and the GitHub deploy role. HCP Terraform owns the bucket, CloudFront, DNS, and the GitHub deploy role.
GitHub Actions owns content: `.github/workflows/deploy-web.yaml` syncs `dist/` GitHub Actions owns content: `.github/workflows/deploy-web.yaml` syncs `dist/`
to the bucket root and invalidates `/*`. to the bucket root and invalidates `/*`.
@ -9,16 +9,19 @@ Creating, formatting, initializing with `-backend=false`, and validating these
files does not authorize an AWS, HCP Terraform, GitHub, or deployment files does not authorize an AWS, HCP Terraform, GitHub, or deployment
mutation. Live cutover waits for an explicit greenlight. mutation. Live cutover waits for an explicit greenlight.
Do not collapse these roots into one `terraform/` tree in this PR. Flattening
retargets two live HCP working directories and is its own change.
## Fixed targets ## Fixed targets
| | dev | staging | | | dev | staging |
| ----------------------- | ------------------------------------ | ---------------------------------------- | | ------------- | ------------------------------------ | ---------------------------------------- |
| Site | `dev.seahaven.com` | `staging.seahaven.com` | | Site | `dev.seahaven.com` | `staging.seahaven.com` |
| Bucket | `seahaven-shoc-frontend-dev` | `seahaven-shoc-frontend-staging` | | Bucket | `seahaven-shoc-frontend-dev` | `seahaven-shoc-frontend-staging` |
| Distribution | `E2CWLM1AFB964P` | `E2JDVEZ6EGD49J` | | Distribution | `E2CWLM1AFB964P` | `E2JDVEZ6EGD49J` |
| Deploy role | `githubdeploy-shoc-frontend-new-dev` | `githubdeploy-shoc-frontend-new-staging` | | Deploy role | `githubdeploy-shoc-frontend-new-dev` | `githubdeploy-shoc-frontend-new-staging` |
| HCP workspace | `shoc-frontend-new-dev` | `shoc-frontend-new-staging` | | HCP workspace | `shoc-frontend-new-dev` | `shoc-frontend-new-staging` |
| Workspace `environment` | `dev` | `staging` | | Working dir | `terraform/live/dev` | `terraform/live/staging` |
There is no prod CloudFront in this round. Do not create There is no prod CloudFront in this round. Do not create
`shoc-frontend-new-prod`. `shoc-frontend-new-prod`.
@ -35,15 +38,17 @@ GetParameter on those two names. OIDC trust is `environment:<env>` plus
`job_workflow_ref` for `.github/workflows/deploy-web.yaml` at `refs/heads/main` `job_workflow_ref` for `.github/workflows/deploy-web.yaml` at `refs/heads/main`
and `refs/tags/v*`. and `refs/tags/v*`.
`adoption_complete` is pinned per stack in `locals.tf`. It is not a workspace `adoption_complete` is pinned in each live root. It is not a workspace
variable. variable.
## Local checks (no apply) ## Local checks (no apply)
```bash ```bash
terraform -chdir=terraform fmt -check -recursive terraform fmt -check -recursive terraform
terraform -chdir=terraform init -backend=false -lockfile=readonly terraform -chdir=terraform/live/dev init -backend=false -lockfile=readonly
TF_VAR_environment=dev terraform -chdir=terraform validate terraform -chdir=terraform/live/dev validate
terraform -chdir=terraform/live/staging init -backend=false -lockfile=readonly
terraform -chdir=terraform/live/staging validate
python3 scripts/test-terraform-import-plan-check.py python3 scripts/test-terraform-import-plan-check.py
python3 scripts/test_check_app_terraform_isolation.py python3 scripts/test_check_app_terraform_isolation.py
bash scripts/test-verify-cloudfront-release.sh bash scripts/test-verify-cloudfront-release.sh
@ -58,13 +63,12 @@ create an HCP run or touch AWS.
## Cutover (greenlight only) ## Cutover (greenlight only)
1. Point both HCP workspaces at working directory `terraform/` with tag 1. Keep HCP working directories `terraform/live/dev` and
`app:shoc-frontend-new`. Dev VCS branch `main`. Staging tag regex `terraform/live/staging`. Dev VCS branch `main`. Staging tag regex
`^v[0-9]+\.[0-9]+\.[0-9]+-staging$`. `^v[0-9]+\.[0-9]+\.[0-9]+-staging$`.
2. Set workspace variable `environment` to `dev` or `staging`. 2. Auto-apply off. Apply the origin-path move for **dev** before any
3. Auto-apply off. Apply the origin-path move for **dev** before any
`--delete` root sync. `--delete` root sync.
4. Create GitHub Environment `dev` (staging already exists). Set 3. Create GitHub Environment `dev` (staging already exists). Set
`DEPLOY_ROLE_ARN` on each. `DEPLOY_ROLE_ARN` on each.
5. Enable `deploy-web.yaml`. Then retire `deploy.yml`, `TF_API_TOKEN`, and 4. Enable `deploy-web.yaml`. Then retire `deploy.yml`, `TF_API_TOKEN`, and
`TERRAFORM_CONTENT_CD_ENABLED`. `TERRAFORM_CONTENT_CD_ENABLED`.

8
terraform/live/README.md Normal file
View file

@ -0,0 +1,8 @@
# Live Terraform roots
`live/dev/` is the adopted HCP workspace `shoc-frontend-new-dev`.
`live/staging/` is `shoc-frontend-new-staging` (`adoption_complete = false`).
Do not collapse these into one `terraform/` root in the same PR as application
CD. Flattening retargets two live HCP working directories and belongs in its
own change.

View file

@ -1,64 +1,64 @@
import { import {
to = module.environment_owned.aws_s3_bucket.site to = module.environment_owned.aws_s3_bucket.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_s3_bucket_public_access_block.site to = module.environment_owned.aws_s3_bucket_public_access_block.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_s3_bucket_ownership_controls.site to = module.environment_owned.aws_s3_bucket_ownership_controls.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_s3_bucket_server_side_encryption_configuration.site to = module.environment_owned.aws_s3_bucket_server_side_encryption_configuration.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_s3_bucket_versioning.site to = module.environment_owned.aws_s3_bucket_versioning.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_s3_bucket_policy.site to = module.environment_owned.aws_s3_bucket_policy.site
id = local.stack.bucket_name id = local.bucket_name
} }
import { import {
to = module.environment_owned.aws_cloudfront_distribution.site to = module.environment_owned.aws_cloudfront_distribution.site
id = local.stack.distribution_id id = local.distribution_id
} }
import { import {
to = module.environment_owned.aws_cloudfront_origin_access_control.site to = module.environment_owned.aws_cloudfront_origin_access_control.site
id = local.stack.oac_id id = local.oac_id
} }
import { import {
to = module.environment_owned.aws_cloudfront_function.spa_rewrite to = module.environment_owned.aws_cloudfront_function.spa_rewrite
id = local.stack.function_name id = local.function_name
} }
import { import {
to = module.environment_owned.aws_route53_record.site_a to = module.environment_owned.aws_route53_record.site_a
id = "${local.stack.hosted_zone_id}_${local.stack.domain_name}_A" id = "${local.hosted_zone_id}_${local.domain_name}_A"
} }
import { import {
to = module.environment_owned.aws_route53_record.site_aaaa to = module.environment_owned.aws_route53_record.site_aaaa
id = "${local.stack.hosted_zone_id}_${local.stack.domain_name}_AAAA" id = "${local.hosted_zone_id}_${local.domain_name}_AAAA"
} }
import { import {
to = module.environment_owned.aws_iam_role.github_deploy to = module.environment_owned.aws_iam_role.github_deploy
id = local.stack.deploy_role_name id = local.deploy_role_name
} }
import { import {
to = module.environment_owned.aws_iam_role_policy.github_deploy to = module.environment_owned.aws_iam_role_policy.github_deploy
id = "${local.stack.deploy_role_name}:${local.stack.inline_policy}" id = "${local.deploy_role_name}:${local.inline_policy}"
} }

View file

@ -0,0 +1,95 @@
locals {
# Controlled ownership transfer. Pinned in code, never a workspace variable.
adoption_complete = true
environment = "dev"
workspace_name = "shoc-frontend-new-dev"
github_repo = "Sea-Haven-Industries/shoc-frontend-new"
aws_account_id = "396287094661"
aws_region = "us-east-1"
bucket_name = "seahaven-shoc-frontend-dev"
distribution_id = "E2CWLM1AFB964P"
oac_id = "E30VSIK87N8H64"
oac_name = "shocfrontenddevDistributionOrigin1S3OriginAccessControlDFC82620"
origin_id = "shocfrontenddevDistributionOrigin10CCD0EE1"
function_name = "us-east-1shocfrontenddevSpaRewrite58674DB8"
domain_name = "dev.seahaven.com"
hosted_zone_id = "Z07671212N75U4YLPWZR8"
certificate_arn = (
"arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
)
github_oidc_arn = (
"arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com"
)
deploy_role_name = "githubdeploy-shoc-frontend-new-dev"
inline_policy = "GithubDeployRoleDefaultPolicyE8F540D1"
stack_name = "shoc-frontend-dev"
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
permissions_boundary_arn = (
"arn:aws:iam::396287094661:policy/shoc-frontend-new-dev-deploy-boundary"
)
bucket_auto_delete_helper_role_arn = (
"arn:aws:iam::396287094661:role/shoc-frontend-dev-CustomS3AutoDeleteObjectsCustomRe-dmSDIY8EH7KV"
)
legacy_tags = {
Environment = "dev"
ManagedBy = "cdk"
Project = "shoc-frontend"
}
legacy_bucket_tags = merge(local.legacy_tags, {
"aws-cdk:auto-delete-objects" = "true"
})
terraform_tags = {
Environment = "dev"
ManagedBy = "terraform"
Ownership = "terraform"
Project = "shoc-frontend"
}
manager_tag = {
HcpTerraformWorkspace = local.workspace_name
}
}
module "inventory" {
source = "../modules/environment-inventory"
aws_account_id = local.aws_account_id
aws_region = local.aws_region
hosted_zone_name = local.domain_name
expected_hosted_zone_id = local.hosted_zone_id
certificate_domain = "*.seahaven.com"
expected_certificate_arn = local.certificate_arn
expected_github_oidc_provider_arn = local.github_oidc_arn
expected_cache_policy_id = local.cache_policy_id
}
module "environment_owned" {
source = "../modules/environment-owned"
environment = local.environment
adoption_complete = local.adoption_complete
aws_account_id = local.aws_account_id
aws_region = local.aws_region
github_repo = local.github_repo
bucket_name = local.bucket_name
distribution_id = local.distribution_id
origin_access_control_name = local.oac_name
origin_access_control_description = ""
origin_id = local.origin_id
function_name = local.function_name
domain_name = local.domain_name
hosted_zone_id = local.hosted_zone_id
certificate_arn = local.certificate_arn
cache_policy_id = local.cache_policy_id
github_oidc_provider_arn = local.github_oidc_arn
deploy_role_name = local.deploy_role_name
deploy_inline_policy_name = local.inline_policy
deploy_permissions_boundary_arn = local.permissions_boundary_arn
cloudformation_stack_name = local.stack_name
bucket_auto_delete_helper_role_arn = local.bucket_auto_delete_helper_role_arn
pre_adoption_tags = local.legacy_tags
pre_adoption_bucket_tags = local.legacy_bucket_tags
ownership_tags = local.terraform_tags
pre_adoption_deploy_role_tags = merge(local.legacy_tags, local.manager_tag)
post_adoption_deploy_role_tags = merge(local.terraform_tags, local.manager_tag)
}

View file

@ -1,18 +1,19 @@
terraform { terraform {
required_version = ">= 1.14.0, < 2.0.0" required_version = ">= 1.14.0, < 2.0.0"
cloud {
organization = "seahaven"
workspaces {
project = "seahaven-external-dev"
name = "shoc-frontend-new-dev"
}
}
required_providers { required_providers {
aws = { aws = {
source = "hashicorp/aws" source = "hashicorp/aws"
version = "~> 6.57" version = "~> 6.57"
} }
} }
cloud {
organization = "seahaven"
workspaces {
tags = ["app:shoc-frontend-new"]
}
}
} }

View file

@ -0,0 +1,30 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "6.62.0"
constraints = "~> 6.57"
hashes = [
"h1:4qcuRkosNKYxV2y69uJ6zAfTEO1Op04L4KUuWBrUvBo=",
"h1:OthB9UeoBgmy348EpDjs5GDGk6p6UxAMQD5cXn7u9Ho=",
"h1:lTKd2c1EunGxt2XROLgEeSXA2Jk+WiiG9BTcp+L/0xY=",
"h1:nWSI/kgPk9aieiY01TEKOGXRX3+L889GSkEq0SMCL6E=",
"h1:yOSEz5G8b/n5uhFCZ0gbEsKkAQATtVuhXJEXR3OM5qs=",
"zh:35a9e4bc6fd622c5a99561b882025f2745f1256bbf1a8da8d6b39319b75ae0b5",
"zh:405927d470ff16201e40aa0fa2d0ab1de477360a0926d20719cd029179682ecd",
"zh:4ab7866593a90bcf18f066b0092a209b9f42852acd783b504031ae74cb6f7010",
"zh:5b477f313fc511648a4eed9f9085d0778414835896256ab14296d2345b7070e3",
"zh:87de70bc99751f94262cec2260d972555a98f588aa3a613e417438f88a1182df",
"zh:88f02a8ff07f00da4ffb3bee9e8ae25588e3a0a92633c625c1c2a63bac00a844",
"zh:8d8596257453357c9f3fccaa7d2f04299e8d35b16f364adb8a2c829143a9c090",
"zh:953c8e15fa9c12c081f17d66cf45246d032fa23bee33f264dc82242afdb98bc2",
"zh:9a7dd903e5e9b2b0cc1317ad2d2692e0ddf05ae2a5aaee20ec5dd1db456711b7",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:a859154c75c1088d098a481f1ebb259720c5a2ad87781364abf556a741e5adb7",
"zh:b8d1e72ad39d5864118f64dd3273424ab637d34b3ff8dd3dfeb4aef9d458587f",
"zh:c3666fcfc7b131f5282d4e7249fa68c3a21665757888aa02bbaf6be1cd036bba",
"zh:c6b8ff94b3f49bf85fc087381cfe1b271c5b01cf74cf140d58aa500be7138913",
"zh:d8143d790e9dd77b8e2f9168e4a33ad6d064dc4b082a0196636b182105aaed14",
"zh:fa41eca042f377eb2741e95b36609c1de5b0cd675cd4e3e30c709497cb94db02",
]
}

View file

@ -0,0 +1,64 @@
import {
to = module.environment_owned.aws_s3_bucket.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_public_access_block.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_ownership_controls.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_server_side_encryption_configuration.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_versioning.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_policy.site
id = local.bucket_name
}
import {
to = module.environment_owned.aws_cloudfront_distribution.site
id = local.distribution_id
}
import {
to = module.environment_owned.aws_cloudfront_origin_access_control.site
id = local.oac_id
}
import {
to = module.environment_owned.aws_cloudfront_function.spa_rewrite
id = local.function_name
}
import {
to = module.environment_owned.aws_route53_record.site_a
id = "${local.hosted_zone_id}_${local.domain_name}_A"
}
import {
to = module.environment_owned.aws_route53_record.site_aaaa
id = "${local.hosted_zone_id}_${local.domain_name}_AAAA"
}
import {
to = module.environment_owned.aws_iam_role.github_deploy
id = local.deploy_role_name
}
import {
to = module.environment_owned.aws_iam_role_policy.github_deploy
id = "${local.deploy_role_name}:${local.inline_policy}"
}

View file

@ -0,0 +1,95 @@
locals {
# Staging is not fully adopted. Pinned in code, never a workspace variable.
adoption_complete = false
environment = "staging"
workspace_name = "shoc-frontend-new-staging"
github_repo = "Sea-Haven-Industries/shoc-frontend-new"
aws_account_id = "396287094661"
aws_region = "us-east-1"
bucket_name = "seahaven-shoc-frontend-staging"
distribution_id = "E2JDVEZ6EGD49J"
oac_id = "E1PF5R6QQNBZAI"
oac_name = "shocfrontendstagingDistributOrigin1S3OriginAccessControl82B1C17D"
origin_id = "shocfrontendstagingDistributionOrigin16E4628FC"
function_name = "us-east-1shocfrontendstagingSpaRewriteE9C0CBDA"
domain_name = "staging.seahaven.com"
hosted_zone_id = "Z02602739VQWBWCAGXP4"
certificate_arn = (
"arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
)
github_oidc_arn = (
"arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com"
)
deploy_role_name = "githubdeploy-shoc-frontend-new-staging"
inline_policy = "GithubDeployRoleDefaultPolicyE8F540D1"
stack_name = "shoc-frontend-staging"
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
permissions_boundary_arn = (
"arn:aws:iam::396287094661:policy/shoc-frontend-new-staging-deploy-boundary"
)
bucket_auto_delete_helper_role_arn = (
"arn:aws:iam::396287094661:role/shoc-frontend-staging-CustomS3AutoDeleteObjectsCust-QbMDqZbl7YQ3"
)
legacy_tags = {
Environment = "staging"
ManagedBy = "cdk"
Project = "shoc-frontend"
}
legacy_bucket_tags = merge(local.legacy_tags, {
"aws-cdk:auto-delete-objects" = "true"
})
terraform_tags = {
Environment = "staging"
ManagedBy = "terraform"
Ownership = "terraform"
Project = "shoc-frontend"
}
manager_tag = {
HcpTerraformWorkspace = local.workspace_name
}
}
module "inventory" {
source = "../modules/environment-inventory"
aws_account_id = local.aws_account_id
aws_region = local.aws_region
hosted_zone_name = local.domain_name
expected_hosted_zone_id = local.hosted_zone_id
certificate_domain = "*.seahaven.com"
expected_certificate_arn = local.certificate_arn
expected_github_oidc_provider_arn = local.github_oidc_arn
expected_cache_policy_id = local.cache_policy_id
}
module "environment_owned" {
source = "../modules/environment-owned"
environment = local.environment
adoption_complete = local.adoption_complete
aws_account_id = local.aws_account_id
aws_region = local.aws_region
github_repo = local.github_repo
bucket_name = local.bucket_name
distribution_id = local.distribution_id
origin_access_control_name = local.oac_name
origin_access_control_description = ""
origin_id = local.origin_id
function_name = local.function_name
domain_name = local.domain_name
hosted_zone_id = local.hosted_zone_id
certificate_arn = local.certificate_arn
cache_policy_id = local.cache_policy_id
github_oidc_provider_arn = local.github_oidc_arn
deploy_role_name = local.deploy_role_name
deploy_inline_policy_name = local.inline_policy
deploy_permissions_boundary_arn = local.permissions_boundary_arn
cloudformation_stack_name = local.stack_name
bucket_auto_delete_helper_role_arn = local.bucket_auto_delete_helper_role_arn
pre_adoption_tags = local.legacy_tags
pre_adoption_bucket_tags = local.legacy_bucket_tags
ownership_tags = local.terraform_tags
pre_adoption_deploy_role_tags = merge(local.legacy_tags, local.manager_tag)
post_adoption_deploy_role_tags = merge(local.terraform_tags, local.manager_tag)
}

View file

@ -0,0 +1,19 @@
output "bucket_name" {
value = module.environment_owned.bucket_name
description = "SPA origin bucket name."
}
output "distribution_id" {
value = module.environment_owned.distribution_id
description = "CloudFront distribution ID."
}
output "deploy_role_arn" {
value = module.environment_owned.deploy_role_arn
description = "GitHub Actions deploy role ARN."
}
output "origin_id" {
value = module.environment_owned.origin_id
description = "CloudFront origin ID for the bucket-root SPA."
}

View file

@ -0,0 +1,3 @@
provider "aws" {
region = local.aws_region
}

View file

@ -0,0 +1,19 @@
terraform {
required_version = ">= 1.14.0, < 2.0.0"
cloud {
organization = "seahaven"
workspaces {
project = "seahaven-external-dev"
name = "shoc-frontend-new-staging"
}
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.57"
}
}
}

View file

@ -1,74 +0,0 @@
locals {
aws_account_id = "396287094661"
aws_region = "us-east-1"
github_repo = "Sea-Haven-Industries/shoc-frontend-new"
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
github_oidc_arn = (
"arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com"
)
certificate_arn = (
"arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
)
stacks = {
dev = {
# Pinned in code, never a workspace variable.
adoption_complete = true
bucket_name = "seahaven-shoc-frontend-dev"
distribution_id = "E2CWLM1AFB964P"
oac_id = "E30VSIK87N8H64"
oac_name = "shocfrontenddevDistributionOrigin1S3OriginAccessControlDFC82620"
origin_id = "shocfrontenddevDistributionOrigin10CCD0EE1"
function_name = "us-east-1shocfrontenddevSpaRewrite58674DB8"
domain_name = "dev.seahaven.com"
hosted_zone_id = "Z07671212N75U4YLPWZR8"
deploy_role_name = "githubdeploy-shoc-frontend-new-dev"
inline_policy = "GithubDeployRoleDefaultPolicyE8F540D1"
workspace_name = "shoc-frontend-new-dev"
stack_name = "shoc-frontend-dev"
permissions_boundary_arn = (
"arn:aws:iam::396287094661:policy/shoc-frontend-new-dev-deploy-boundary"
)
bucket_auto_delete_helper_role_arn = (
"arn:aws:iam::396287094661:role/shoc-frontend-dev-CustomS3AutoDeleteObjectsCustomRe-dmSDIY8EH7KV"
)
}
staging = {
adoption_complete = false
bucket_name = "seahaven-shoc-frontend-staging"
distribution_id = "E2JDVEZ6EGD49J"
oac_id = "E1PF5R6QQNBZAI"
oac_name = "shocfrontendstagingDistributOrigin1S3OriginAccessControl82B1C17D"
origin_id = "shocfrontendstagingDistributionOrigin16E4628FC"
function_name = "us-east-1shocfrontendstagingSpaRewriteE9C0CBDA"
domain_name = "staging.seahaven.com"
hosted_zone_id = "Z02602739VQWBWCAGXP4"
deploy_role_name = "githubdeploy-shoc-frontend-new-staging"
inline_policy = "GithubDeployRoleDefaultPolicyE8F540D1"
workspace_name = "shoc-frontend-new-staging"
stack_name = "shoc-frontend-staging"
permissions_boundary_arn = (
"arn:aws:iam::396287094661:policy/shoc-frontend-new-staging-deploy-boundary"
)
bucket_auto_delete_helper_role_arn = (
"arn:aws:iam::396287094661:role/shoc-frontend-staging-CustomS3AutoDeleteObjectsCust-QbMDqZbl7YQ3"
)
}
}
stack = local.stacks[var.environment]
legacy_tags = {
Environment = var.environment
ManagedBy = "cdk"
Project = "shoc-frontend"
}
terraform_tags = {
Environment = var.environment
ManagedBy = "terraform"
Ownership = "terraform"
Project = "shoc-frontend"
}
manager_tag = {
HcpTerraformWorkspace = local.stack.workspace_name
}
}

View file

@ -1,45 +0,0 @@
module "inventory" {
source = "./modules/environment-inventory"
aws_account_id = local.aws_account_id
aws_region = local.aws_region
hosted_zone_name = local.stack.domain_name
expected_hosted_zone_id = local.stack.hosted_zone_id
certificate_domain = "*.seahaven.com"
expected_certificate_arn = local.certificate_arn
expected_github_oidc_provider_arn = local.github_oidc_arn
expected_cache_policy_id = local.cache_policy_id
}
module "environment_owned" {
source = "./modules/environment-owned"
environment = var.environment
adoption_complete = local.stack.adoption_complete
aws_account_id = local.aws_account_id
aws_region = local.aws_region
github_repo = local.github_repo
bucket_name = local.stack.bucket_name
distribution_id = local.stack.distribution_id
origin_access_control_name = local.stack.oac_name
origin_access_control_description = ""
origin_id = local.stack.origin_id
function_name = local.stack.function_name
domain_name = local.stack.domain_name
hosted_zone_id = local.stack.hosted_zone_id
certificate_arn = local.certificate_arn
cache_policy_id = local.cache_policy_id
github_oidc_provider_arn = local.github_oidc_arn
deploy_role_name = local.stack.deploy_role_name
deploy_inline_policy_name = local.stack.inline_policy
deploy_permissions_boundary_arn = local.stack.permissions_boundary_arn
cloudformation_stack_name = local.stack.stack_name
bucket_auto_delete_helper_role_arn = local.stack.bucket_auto_delete_helper_role_arn
pre_adoption_tags = local.legacy_tags
pre_adoption_bucket_tags = merge(local.legacy_tags, {
"aws-cdk:auto-delete-objects" = "true"
})
ownership_tags = local.terraform_tags
pre_adoption_deploy_role_tags = merge(local.legacy_tags, local.manager_tag)
post_adoption_deploy_role_tags = merge(local.terraform_tags, local.manager_tag)
}

View file

@ -1,9 +0,0 @@
variable "environment" {
description = "Workspace environment. HCP Terraform sets this per workspace."
type = string
validation {
condition = contains(["dev", "staging"], var.environment)
error_message = "environment must be one of: dev, staging."
}
}