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
run: terraform fmt -check -recursive terraform
- name: Terraform init
run: terraform -chdir=terraform init -backend=false -input=false -lockfile=readonly -no-color
- name: Validate live/dev
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
run: terraform -chdir=terraform validate -no-color
env:
TF_VAR_environment: dev
- name: Validate live/staging
run: |
terraform -chdir=terraform/live/staging init -backend=false -input=false -lockfile=readonly -no-color
terraform -chdir=terraform/live/staging validate -no-color
- name: Import plan guard tests
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 | Command / rule source | Enforced by | Scope |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------------- |
| 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`) |
| 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` |
| 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 |
| 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 |
| 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 |
| 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/` |
| 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 |
| 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/` |
| Gate | Command / rule source | Enforced by | Scope |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------- |
| 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`) |
| 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` |
| 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 |
| 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 |
| 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 |
| 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/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 |
| 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 |
| 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

View file

@ -34,9 +34,9 @@ graph LR
TF[HCP Terraform] -->|bucket CloudFront IAM SSM| CF
```
Dev and staging hosting live in one `terraform/` tree. GitHub
`.github/workflows/deploy-web.yaml` syncs content. The older pointer CD
(`deploy.yml`) stays in the tree until cutover.
Dev and staging hosting live in `terraform/live/dev` and
`terraform/live/staging`. GitHub `.github/workflows/deploy-web.yaml` syncs
content. The older pointer CD (`deploy.yml`) stays in the tree until cutover.
Frontend stack: React 19, TypeScript, Vite, Tailwind CSS 4 + MUI, TanStack
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
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 |
| ----------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
@ -84,7 +84,7 @@ build otherwise. See [`.env.example`](.env.example),
[`.env.development`](.env.development), and [`.env.production`](.env.production).
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

View file

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

View file

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

View file

@ -13,8 +13,8 @@ class IsolationTests(unittest.TestCase):
self.assertIsNone(
isolation_violation(
[
"terraform/main.tf",
"terraform/README.md",
"terraform/live/dev/main.tf",
"terraform/live/README.md",
]
)
)
@ -35,7 +35,7 @@ class IsolationTests(unittest.TestCase):
self.assertIsNone(
isolation_violation(
[
"terraform/modules/environment-owned/main.tf",
"terraform/live/modules/environment-owned/main.tf",
".github/workflows/deploy-web.yaml",
"QUALITY_GATES.md",
"scripts/governance-check.mjs",
@ -48,21 +48,21 @@ class IsolationTests(unittest.TestCase):
def test_mixed_src_and_terraform_fails(self) -> None:
violation = isolation_violation(
[
"terraform/main.tf",
"terraform/live/dev/main.tf",
"src/app/routes.tsx",
]
)
self.assertIsNotNone(violation)
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"])
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)
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)

View file

@ -1,6 +1,6 @@
# 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.
GitHub Actions owns content: `.github/workflows/deploy-web.yaml` syncs `dist/`
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
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
| | dev | staging |
| ----------------------- | ------------------------------------ | ---------------------------------------- |
| Site | `dev.seahaven.com` | `staging.seahaven.com` |
| Bucket | `seahaven-shoc-frontend-dev` | `seahaven-shoc-frontend-staging` |
| Distribution | `E2CWLM1AFB964P` | `E2JDVEZ6EGD49J` |
| Deploy role | `githubdeploy-shoc-frontend-new-dev` | `githubdeploy-shoc-frontend-new-staging` |
| HCP workspace | `shoc-frontend-new-dev` | `shoc-frontend-new-staging` |
| Workspace `environment` | `dev` | `staging` |
| | dev | staging |
| ------------- | ------------------------------------ | ---------------------------------------- |
| Site | `dev.seahaven.com` | `staging.seahaven.com` |
| Bucket | `seahaven-shoc-frontend-dev` | `seahaven-shoc-frontend-staging` |
| Distribution | `E2CWLM1AFB964P` | `E2JDVEZ6EGD49J` |
| Deploy role | `githubdeploy-shoc-frontend-new-dev` | `githubdeploy-shoc-frontend-new-staging` |
| HCP workspace | `shoc-frontend-new-dev` | `shoc-frontend-new-staging` |
| Working dir | `terraform/live/dev` | `terraform/live/staging` |
There is no prod CloudFront in this round. Do not create
`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`
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.
## Local checks (no apply)
```bash
terraform -chdir=terraform fmt -check -recursive
terraform -chdir=terraform init -backend=false -lockfile=readonly
TF_VAR_environment=dev terraform -chdir=terraform validate
terraform fmt -check -recursive terraform
terraform -chdir=terraform/live/dev init -backend=false -lockfile=readonly
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_check_app_terraform_isolation.py
bash scripts/test-verify-cloudfront-release.sh
@ -58,13 +63,12 @@ create an HCP run or touch AWS.
## Cutover (greenlight only)
1. Point both HCP workspaces at working directory `terraform/` with tag
`app:shoc-frontend-new`. Dev VCS branch `main`. Staging tag regex
1. Keep HCP working directories `terraform/live/dev` and
`terraform/live/staging`. Dev VCS branch `main`. Staging tag regex
`^v[0-9]+\.[0-9]+\.[0-9]+-staging$`.
2. Set workspace variable `environment` to `dev` or `staging`.
3. Auto-apply off. Apply the origin-path move for **dev** before any
2. Auto-apply off. Apply the origin-path move for **dev** before any
`--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.
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`.

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 {
to = module.environment_owned.aws_s3_bucket.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_public_access_block.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_ownership_controls.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_server_side_encryption_configuration.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_versioning.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_s3_bucket_policy.site
id = local.stack.bucket_name
id = local.bucket_name
}
import {
to = module.environment_owned.aws_cloudfront_distribution.site
id = local.stack.distribution_id
id = local.distribution_id
}
import {
to = module.environment_owned.aws_cloudfront_origin_access_control.site
id = local.stack.oac_id
id = local.oac_id
}
import {
to = module.environment_owned.aws_cloudfront_function.spa_rewrite
id = local.stack.function_name
id = local.function_name
}
import {
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 {
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 {
to = module.environment_owned.aws_iam_role.github_deploy
id = local.stack.deploy_role_name
id = local.deploy_role_name
}
import {
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 {
required_version = ">= 1.14.0, < 2.0.0"
cloud {
organization = "seahaven"
workspaces {
project = "seahaven-external-dev"
name = "shoc-frontend-new-dev"
}
}
required_providers {
aws = {
source = "hashicorp/aws"
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."
}
}