shoc-frontend-new/infra/cdk/README.md
Alexandre Brandizzi 4df6e76192
ci: add protected staging frontend deployment lane (#151)
* ci: add protected staging deployment lane

* fix: constrain staging publisher permissions

* fix: handle first-push governance baseline

---------

Co-authored-by: Codex Review Integration <codex-review@local.invalid>
2026-08-28 10:59:55 -04:00

221 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Infrastructure & CI/CD — Sea Haven SHOC frontend
AWS hosting for the Vite SPA, defined as an **AWS CDK** app local to this repo,
deployed through the org's **reusable** GitHub Actions workflow.
- **Hosting:** private S3 bucket (origin) + CloudFront, served on the custom
domain **`dev.seahaven.com`** (ACM `*.seahaven.com`, Route 53 apex alias).
- **API:** the SPA calls the backend **directly** over HTTPS at
`https://api.dev.seahaven.com/api` (`VITE_API_URL`, cross-origin; the backend
allows CORS). CloudFront serves static content only — no `/api` proxy.
- Domain/cert/zone values live in `cdk.json` context so the CI `cdk deploy`
picks them up with no flags. `VITE_API_URL` is baked into the build, so it's
per-environment (see the note under "Adding staging / prod").
- **Auth:** GitHub Actions → AWS via **OIDC** (no long-lived keys)
- **CD workflow:** `.github/workflows/deploy.yml` is a thin caller of the org's
`Sea-Haven-Industries/.github` → `cd-cdk.yaml`. That workflow runs `cdk deploy`
(provisions infra) then `scripts/deploy-web.sh` (builds + uploads the SPA).
- **Infra is local to this repo** (CDK in `infra/cdk`); the deploy role is
created by this stack, not added to the central `oidc-deploy-roles.yaml`.
- **Environments:** `dev` (push to `dev`, via the org reusable workflow) and
`staging` (push to `staging`, via the standalone `deploy-staging.yml`).
```
infra/cdk/
bin/app.ts entry point (reads -c context)
lib/frontend-stack.ts S3 + CloudFront + OAC + OIDC deploy role
scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation
.github/workflows/
ci.yaml quality gates (lint / build / test / e2e)
deploy.yml caller of the org reusable cd-cdk.yaml (push to dev)
deploy-staging.yml standalone staging deploy (push to staging)
```
## What the stack creates
| Resource | Purpose |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| S3 bucket `seahaven-shoc-frontend-dev` | private origin (BLOCK_ALL, SSE, OAC-only reads) |
| CloudFront distribution | HTTPS, gzip/br; serves the static SPA from S3 (the app calls the API directly, cross-origin) |
| CloudFront Function (viewer request) | SPA routing: rewrites extensionless paths to `/index.html` (scoped to the S3 behavior, so it never touches `/api`) |
| IAM role `githubdeploy-shoc-frontend-new-dev` | assumed by GitHub Actions via OIDC, scoped to `repo:Sea-Haven-Industries/shoc-frontend-new:ref:refs/heads/dev` |
The whole `cd-cdk.yaml` job runs as that role, so it holds: `sts:AssumeRole` on
`cdk-hnb659fds-*` (for `cdk deploy`), `cloudformation:DescribeStacks` (cd-cdk's
pre-flight/health-check + output reads), read/write on the bucket (`s3 sync`),
and `cloudfront:CreateInvalidation` (cache bust). The OIDC **provider** is a
singleton account resource — the stack only _imports_ it (created in step 2),
so `cdk destroy` can't delete a resource shared by other roles.
---
## One-time setup (run by a human with admin AWS creds)
### 1. Authenticate to the AWS account
```bash
aws configure # or: aws sso login --profile <admin>
aws sts get-caller-identity # confirm the right account + region (us-east-1)
```
### 2. Ensure the GitHub OIDC provider exists (once per account)
```bash
aws iam list-open-id-connect-providers
# If none ends in token.actions.githubusercontent.com, create it (thumbprint is
# no longer required — AWS validates GitHub against its own trust store):
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
```
### 3. CDK bootstrap (once per account/region)
```bash
cd infra/cdk
npm ci
npx cdk bootstrap aws://<ACCOUNT_ID>/us-east-1
```
### 4. Domain, cert, and API URL (already wired for dev)
Domain/cert/zone are set in `cdk.json` context (account `396287094661`):
| Context key | Value |
| --------------------------------- | ------------------------------------------------------------ |
| `domainNames` | `dev.seahaven.com` |
| `certificateArn` | `…:certificate/2b78e74f-…` (ACM `*.seahaven.com`, us-east-1) |
| `hostedZoneId` / `hostedZoneName` | `Z07671212N75U4YLPWZR8` / `dev.seahaven.com` |
The stack creates the apex A/AAAA alias in the hosted zone (in this account,
delegated from the parent `seahaven.com` zone). The **API URL is not infra** —
it's `VITE_API_URL` in `.env.production` (`https://api.dev.seahaven.com/api`),
baked into the build. Per-environment; override for staging/prod.
### 5. First deploy (locally, with admin creds)
The deploy role doesn't exist until the first `cdk deploy`, so bootstrap it
locally. This provisions infra + the role:
```bash
cd infra/cdk
npx cdk deploy
```
Note the `DeployRoleArn` output. Then push the first content (or just push to
`dev` and let CI do everything from here on):
```bash
# from repo root, optional manual first content publish:
STACK_NAME=shoc-frontend-dev AWS_REGION=us-east-1 bash scripts/deploy-web.sh
```
### 6. Set the one GitHub secret
`cd-cdk.yaml` takes the role ARN as a **secret** (not a variable):
```bash
REPO=Sea-Haven-Industries/shoc-frontend-new
gh secret set AWS_DEPLOY_ROLE_ARN --repo "$REPO" \
--body "arn:aws:iam::<acct>:role/githubdeploy-shoc-frontend-new-dev"
```
(Or **Settings → Secrets and variables → Actions → Secrets**.)
### 7. From now on: push to `dev`
```bash
git push origin dev
```
`ci.yml` runs the quality gates and `deploy.yml` calls `cd-cdk.yaml`, which runs
`cdk deploy` then `scripts/deploy-web.sh`. Watch the **Actions** tab, then open
the `SiteUrl` output.
> First-run verification: this first push is what actually exercises the role's
> permissions and the OIDC trust through the reusable workflow (the local
> bootstrap used admin creds and tested none of that). Watch for
> credential/OIDC errors and a green post-deploy step.
---
## Staging environment (same account, exact OIDC subject)
Staging lives in the same AWS account (396287094661) but deploys through its
own standalone workflow, `.github/workflows/deploy-staging.yml`, not the org
reusable `cd-cdk.yaml`:
- **Trust:** with `-c githubEnvironment=staging`, the stack's deploy role
(`githubdeploy-shoc-frontend-new-staging`) trusts ONLY the exact GitHub
environment subject
`repo:Sea-Haven-Industries/shoc-frontend-new:environment:staging`
(`StringEquals` on both `aud` and `sub`). The workflow declares
`environment: staging`, so only runs in that environment can assume the role.
Without `githubEnvironment`, the dev stack keeps its branch-ref trust
unchanged.
- **No secret:** the role ARN is static (the role name is deterministic), so
the workflow pins
`arn:aws:iam::396287094661:role/githubdeploy-shoc-frontend-new-staging`
directly — no `AWS_DEPLOY_ROLE_ARN`-style secret to set.
- **Gates first:** the workflow runs the full `npm run verify` before assuming
the staging role, then runs `scripts/deploy-web.sh` with
`STACK_NAME=shoc-frontend-staging`,
`VITE_API_URL=https://api.staging.seahaven.com/api`, and waits for the
CloudFront invalidation to complete.
- **Application-only role:** the recurring staging workflow can describe only
its exact stack, publish only to its exact bucket, and invalidate only its
exact distribution. It cannot assume the shared CDK bootstrap roles or
modify infrastructure. Staging infrastructure changes use the Administrator
command below.
- **Post-deploy checks:** bucket + distribution existence, HTTPS on
`https://staging.seahaven.com`, and the actual post-invalidation remote assets
contain the staging API URL and no dev API URL. (Not browser QA.)
### One-time setup (run by a human with admin AWS creds + GitHub Admin)
1. **GitHub Admin — create the `staging` environment** (Settings →
Environments → New environment → `staging`). Add protection rules as
appropriate (e.g. required reviewers, restrict to the `staging` branch). If
the environment does not exist, GitHub creates it unprotected on first use.
2. **AWS Admin — first deploy with admin creds** (same steps 1–3 as dev; the
OIDC provider and bootstrap already exist in this account):
```bash
cd infra/cdk
npx cdk deploy shoc-frontend-staging \
-c envName=staging \
-c deployBranch=staging \
-c githubEnvironment=staging \
-c domainNames=staging.seahaven.com \
-c certificateArn=arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00 \
-c hostedZoneId=Z02602739VQWBWCAGXP4 \
-c hostedZoneName=staging.seahaven.com
```
The `DeployRoleArn` output must match the ARN pinned in
`deploy-staging.yml` (it will — the role name is deterministic).
3. **Backend CORS:** the staging API (`https://api.staging.seahaven.com`) must
allow the `https://staging.seahaven.com` origin.
4. Push to `staging` — `ci.yaml` runs the quality gates and
`deploy-staging.yml` deploys.
### Adding prod later
Same pattern: a prod account/stack with its own contexts and, ideally, its own
`githubEnvironment=prod` trust + workflow. Keep in mind `VITE_API_URL` is baked
into each environment's build, and the bucket's `RemovalPolicy.DESTROY` +
`autoDeleteObjects` defaults are dev/staging-friendly but should be revisited
for prod.
## Notes
- **Teardown:** `npx cdk destroy`. The bucket uses `RemovalPolicy.DESTROY` +
`autoDeleteObjects` (dev artifacts are reproducible) — change this for prod.
- **CI and CD both fire on push to `dev` and `staging`** in parallel (staging
differs only in that its CD workflow also runs `npm run verify` itself
before deploying); a red-CI commit still deploys on `dev` (matches the
org's push-time-CD model). Gating dev deploy on CI is a follow-up, not part
of enabling CICD.
- **npm is pinned to v11.16.0**; the committed `package-lock.json` uses
lockfileVersion 3, matching the Node 24 / npm 11 CI environment.