mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
feat(infra): call dev API directly, drop the CloudFront /api proxy
Adopt the direct cross-origin API approach (from feat/cd-cdk-fixes) now that the dev backend allows CORS: - VITE_API_URL=https://api.dev.seahaven.com/api (absolute, baked into the build) - remove the CloudFront /api/* proxy behavior + HTTP origin; CloudFront serves static content only - keep the custom domain (dev.seahaven.com), ACM cert, and Route 53 apex alias VITE_API_URL is per-environment (baked at vite build), so staging/prod builds must override it and the backend must allow their origins.
This commit is contained in:
parent
d1cca884eb
commit
0b8d3fa290
6 changed files with 45 additions and 85 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Production API base (same-origin). CloudFront proxies /api/* to the backend,
|
||||
# so the HTTPS SPA reaches the HTTP-only API without mixed-content blocking.
|
||||
# The backend host is set on the CloudFront /api behavior in infra/cdk.
|
||||
VITE_API_URL=/api
|
||||
# Production API base — the SPA calls the backend directly over HTTPS.
|
||||
# NOTE: baked into the build at `vite build`, so this is the DEV value. Staging
|
||||
# and prod builds must override VITE_API_URL per environment (api.staging..., etc.).
|
||||
VITE_API_URL=https://api.dev.seahaven.com/api
|
||||
|
|
|
|||
|
|
@ -67,9 +67,10 @@ keys):
|
|||
- Push to `dev` → `.github/workflows/deploy.yml` calls the org reusable
|
||||
`cd-cdk.yaml`, which runs `cdk deploy` (infra) then `scripts/deploy-web.sh`
|
||||
(builds the SPA, syncs `dist/` to S3, invalidates CloudFront).
|
||||
- The SPA calls its API same-origin at `/api`; CloudFront proxies `/api/*` to
|
||||
the backend, so the HTTPS app can use the HTTP-only API with no mixed-content
|
||||
blocking.
|
||||
- Served on the custom domain `dev.seahaven.com`; the SPA calls the backend
|
||||
directly over HTTPS at `VITE_API_URL` (`https://api.dev.seahaven.com/api`,
|
||||
cross-origin — the backend allows CORS). `VITE_API_URL` is baked into the
|
||||
build, so it is per-environment.
|
||||
- First-time provisioning (OIDC provider, CDK bootstrap, first local deploy, the
|
||||
`AWS_DEPLOY_ROLE_ARN` secret) is a one-time admin task — see
|
||||
[`infra/cdk/README.md`](infra/cdk/README.md).
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ 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:** CloudFront proxies `/api/*` over HTTPS to `api.dev.seahaven.com`, so
|
||||
the SPA calls it same-origin (`VITE_API_URL=/api`) — no CORS, no mixed content.
|
||||
- Env-specific values (domain, cert, zone, API host) live in `cdk.json` context
|
||||
so the CI `cdk deploy` picks them up with no flags.
|
||||
- **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`
|
||||
|
|
@ -32,7 +34,7 @@ scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation
|
|||
| 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 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` |
|
||||
|
||||
|
|
@ -73,21 +75,20 @@ npm ci
|
|||
npx cdk bootstrap aws://<ACCOUNT_ID>/us-east-1
|
||||
```
|
||||
|
||||
### 4. Domain, cert, and API origin (already wired for dev)
|
||||
### 4. Domain, cert, and API URL (already wired for dev)
|
||||
|
||||
For dev these are set in `cdk.json` context (account `396287094661`):
|
||||
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` |
|
||||
| `apiOriginDomain` / `apiOriginProtocol` | `api.dev.seahaven.com` / `https` |
|
||||
| Context key | Value |
|
||||
| --------------------------------- | ------------------------------------------------------------ |
|
||||
| `domainNames` | `dev.seahaven.com` |
|
||||
| `certificateArn` | `…:certificate/2b78e74f-…` (ACM `*.seahaven.com`, us-east-1) |
|
||||
| `hostedZoneId` / `hostedZoneName` | `Z07671212N75U4YLPWZR8` / `dev.seahaven.com` |
|
||||
|
||||
The SPA calls `/api` same-origin; CloudFront proxies `/api/*` over **HTTPS** to
|
||||
`api.dev.seahaven.com`. The stack creates the apex A/AAAA alias in the hosted
|
||||
zone (which is in this account, delegated from the parent `seahaven.com` zone).
|
||||
For staging/prod, override these context keys per environment.
|
||||
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)
|
||||
|
||||
|
|
@ -138,12 +139,16 @@ the `SiteUrl` output.
|
|||
|
||||
## Adding staging / prod later
|
||||
|
||||
Separate accounts: deploy this stack there with `-c envName=staging`,
|
||||
`-c deployBranch=<branch>`, and `-c apiOriginDomain=<that-env's-api-host>`; 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`.
|
||||
Separate accounts: deploy this stack there with per-env `domainNames`,
|
||||
`certificateArn`, `hostedZoneId`/`hostedZoneName` context; set that repo's
|
||||
`AWS_DEPLOY_ROLE_ARN` secret; and add a job to `deploy.yml`.
|
||||
|
||||
Because the SPA calls the API directly at an absolute URL, **`VITE_API_URL` is
|
||||
baked into `vite build`** — so each environment needs its own build with its own
|
||||
API host (e.g. `https://api.staging.seahaven.com/api`). Set it per environment
|
||||
in the deploy job (e.g. export `VITE_API_URL` before the build step) rather than
|
||||
relying on the committed `.env.production` (which carries the dev value). The
|
||||
backend must also allow CORS from each frontend origin.
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,7 @@ const envName = app.node.tryGetContext("envName") ?? "dev";
|
|||
const githubRepo = app.node.tryGetContext("githubRepo") ?? "Sea-Haven-Industries/shoc-frontend-new";
|
||||
const deployBranch = app.node.tryGetContext("deployBranch") ?? "dev";
|
||||
|
||||
// API origin the CloudFront /api behavior proxies to.
|
||||
// Option A (interim, default): the HTTP-only console.seahavenind.com backend.
|
||||
// Option B: -c apiOriginDomain=<seahaven.com API host> -c apiOriginProtocol=https
|
||||
const apiOriginDomain = app.node.tryGetContext("apiOriginDomain") ?? "console.seahavenind.com";
|
||||
const apiOriginProtocol =
|
||||
app.node.tryGetContext("apiOriginProtocol") === "https" ? "https" : "http";
|
||||
|
||||
// Custom domain (Option B). Comma-separated, e.g. -c domainNames=console.seahaven.com
|
||||
// Custom domain. Comma-separated, e.g. -c domainNames=dev.seahaven.com
|
||||
// The ACM cert MUST be in us-east-1 in the SAME account this stack deploys to.
|
||||
const domainNames = (app.node.tryGetContext("domainNames") ?? "")
|
||||
.split(",")
|
||||
|
|
@ -32,8 +25,6 @@ const stack = new FrontendStack(app, `shoc-frontend-${envName}`, {
|
|||
envName,
|
||||
githubRepo,
|
||||
deployBranch,
|
||||
apiOriginDomain,
|
||||
apiOriginProtocol,
|
||||
domainNames,
|
||||
certificateArn,
|
||||
hostedZoneId,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@
|
|||
"//": "dev environment (account 396287094661). CI runs `cdk deploy` with no -c flags, so these live here.",
|
||||
"domainNames": "dev.seahaven.com",
|
||||
"certificateArn": "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00",
|
||||
"apiOriginDomain": "api.dev.seahaven.com",
|
||||
"apiOriginProtocol": "https",
|
||||
"hostedZoneId": "Z07671212N75U4YLPWZR8",
|
||||
"hostedZoneName": "dev.seahaven.com"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,8 @@ export interface FrontendStackProps extends StackProps {
|
|||
/** Git branch whose pushes may deploy (OIDC sub is scoped to this ref). */
|
||||
readonly deployBranch: string;
|
||||
/**
|
||||
* Hostname of the backend API. CloudFront proxies `/api/*` to it (see
|
||||
* `apiOriginProtocol`) so the SPA can call it same-origin at `/api`.
|
||||
*/
|
||||
readonly apiOriginDomain: string;
|
||||
/**
|
||||
* Protocol CloudFront uses to reach the API origin.
|
||||
* - "http" (Option A, interim): HTTP-only backend (console.seahavenind.com)
|
||||
* - "https" (Option B): the seahaven.com HTTPS backend
|
||||
*/
|
||||
readonly apiOriginProtocol: "http" | "https";
|
||||
/**
|
||||
* Custom domain(s) for the distribution, e.g. ["console.seahaven.com"].
|
||||
* Empty = serve on the default *.cloudfront.net domain (Option A).
|
||||
* Custom domain(s) for the distribution, e.g. ["dev.seahaven.com"].
|
||||
* Empty = serve on the default *.cloudfront.net domain.
|
||||
*/
|
||||
readonly domainNames: string[];
|
||||
/**
|
||||
|
|
@ -68,15 +57,12 @@ export class FrontendStack extends Stack {
|
|||
envName,
|
||||
githubRepo,
|
||||
deployBranch,
|
||||
apiOriginDomain,
|
||||
apiOriginProtocol,
|
||||
domainNames,
|
||||
certificateArn,
|
||||
hostedZoneId,
|
||||
hostedZoneName,
|
||||
} = props;
|
||||
|
||||
const useHttpsApiOrigin = apiOriginProtocol === "https";
|
||||
const hasCustomDomain = domainNames.length > 0;
|
||||
if (hasCustomDomain && !certificateArn) {
|
||||
throw new Error(
|
||||
|
|
@ -97,11 +83,9 @@ export class FrontendStack extends Stack {
|
|||
autoDeleteObjects: true,
|
||||
});
|
||||
|
||||
// SPA client-side routing, scoped to the S3 (default) behavior only:
|
||||
// rewrite extensionless paths (e.g. /work-orders) to /index.html. This is
|
||||
// done with a CloudFront Function rather than distribution-wide
|
||||
// customErrorResponses, because those are global and would also turn
|
||||
// legitimate /api 403/404 responses into the SPA shell.
|
||||
// SPA client-side routing: rewrite extensionless paths (e.g. /work-orders)
|
||||
// to /index.html so deep links resolve. Done with a CloudFront Function
|
||||
// rather than customErrorResponses so real asset 404s stay 404s.
|
||||
const spaRewrite = new cloudfront.Function(this, "SpaRewrite", {
|
||||
comment: "SPA routing: rewrite extensionless paths to /index.html",
|
||||
code: cloudfront.FunctionCode.fromInline(
|
||||
|
|
@ -119,7 +103,9 @@ export class FrontendStack extends Stack {
|
|||
),
|
||||
});
|
||||
|
||||
// --- CloudFront: S3 (SPA) default behavior + /api proxy behavior -------
|
||||
// --- CloudFront: serves the static SPA from S3 -------------------------
|
||||
// The SPA calls the backend directly at its absolute HTTPS URL
|
||||
// (VITE_API_URL, cross-origin), so CloudFront hosts only static content.
|
||||
const distribution = new cloudfront.Distribution(this, "Distribution", {
|
||||
comment: `SeaHaven SHOC frontend (${envName})`,
|
||||
defaultRootObject: "index.html",
|
||||
|
|
@ -148,27 +134,6 @@ export class FrontendStack extends Stack {
|
|||
},
|
||||
],
|
||||
},
|
||||
additionalBehaviors: {
|
||||
// Proxy API calls to the backend. The SPA calls same-origin `/api/...`
|
||||
// over HTTPS; CloudFront forwards to the origin over HTTP (Option A) or
|
||||
// HTTPS (Option B). Either way there's no mixed content or CORS.
|
||||
"/api/*": {
|
||||
origin: new origins.HttpOrigin(apiOriginDomain, {
|
||||
protocolPolicy: useHttpsApiOrigin
|
||||
? cloudfront.OriginProtocolPolicy.HTTPS_ONLY
|
||||
: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
}),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
||||
// Forward everything the viewer sent except Host (CloudFront sets Host
|
||||
// to the origin domain so the backend's routing isn't confused).
|
||||
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
||||
compress: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// --- GitHub Actions OIDC deploy role -----------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue