# Infrastructure & CI/CD — SeaHaven 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 (CDN, HTTPS, SPA routing) - **API:** CloudFront proxies `/api/*` to the HTTP-only backend, so the HTTPS SPA calls it same-origin (no mixed-content blocking). `VITE_API_URL=/api`. - **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` only today, deployed on push to the `dev` branch. ``` 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.yml quality gates (lint / build / test / e2e) deploy.yml caller of the org reusable cd-cdk.yaml (push to dev) ``` ## 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; default behavior → S3, `/api/*` → backend (HTTP 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 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:///us-east-1 ``` ### 4. Confirm the backend API origin The app calls its API same-origin at `/api` (`VITE_API_URL=/api` in `.env.production`), and CloudFront proxies `/api/*` to the backend over HTTP. The backend host defaults to `console.seahavenind.com`; override it if the dev API lives elsewhere: ```bash # default is fine for dev; otherwise: npx cdk deploy -c apiOriginDomain= ``` No mixed-content risk: the browser talks HTTPS to CloudFront, and CloudFront talks HTTP to the origin. ### 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:::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. --- ## Adding staging / prod later Separate accounts: deploy this stack there with `-c envName=staging`, `-c deployBranch=`, and `-c apiOriginDomain=`; set that repo's `AWS_DEPLOY_ROLE_ARN` secret; and add a job to `deploy.yml`. Because the SPA calls `/api` same-origin, the API host is per-environment CloudFront config (the `apiOriginDomain` context) — the build output is identical across environments, so nothing env-specific gets baked into `vite build`. ## 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`** in parallel; a red-CI commit still deploys (matches the org's push-time-CD model). Gating deploy on CI is a follow-up, not part of enabling CICD. - **Local npm is pinned to v6**; the committed `package-lock.json` is lockfileVersion 1. CI (Node 24 / npm 11) reads it fine via `npm ci`.