proposal-system/infra/lib/frontend-stack.ts
Adam Moussa bbd185b280
feat(infra): parameterize stacks for multi-env (prod/staging) (#123)
Adds an env config layer resolved from CDK context (`-c env=staging`, default prod)
and threads it through all three stacks so a fully isolated staging environment can
be deployed in the same AWS account.

prod is byte-identical: the prod config reproduces the deployed values exactly and
stackSuffix='' keeps every construct ID, stack name, and physical resource name
unchanged. Verified via synth — prod foundation keeps proposal-system-db /
-uploads / db-credentials / -auth / seahaven; staging suffixes all of them.

- config.ts: EnvConfig (prod + staging, same account) + resolveConfig
- app.ts: env-aware stack naming + config passthrough
- foundation/compute/frontend: ~40 physical names suffixed with config.stackSuffix;
  CORS, Cognito domain/callbacks, alarms email from config; RETAIN / deletionProtection
  gated on config.retainData so staging can be torn down
- cdk.json: register `env` context (default prod)
- post-deploy.sh: STACK_SUFFIX for dynamic stack-name lookup (default prod)

Note: automated staging CI deploy needs a one-line `cdk-context` input added to the
org reusable cd-cdk.yaml (companion change). prod deploy is unaffected (default prod).
2026-06-12 18:04:53 -04:00

57 lines
2.2 KiB
TypeScript

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cloudfrontOrigins from 'aws-cdk-lib/aws-cloudfront-origins';
import { Construct } from 'constructs';
import { EnvConfig } from './config';
export interface FrontendStackProps extends cdk.StackProps {
config: EnvConfig;
}
export class FrontendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: FrontendStackProps) {
super(scope, id, props);
const { config } = props;
// Fix: INF-M5 — enforce HTTPS-only access on S3 bucket
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
bucketName: `proposal-system-web-${this.account}${config.stackSuffix}`,
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
const distribution = new cloudfront.Distribution(this, 'Distribution', {
comment: `proposal-system-web${config.stackSuffix}`,
defaultBehavior: {
origin: cloudfrontOrigins.S3BucketOrigin.withOriginAccessControl(siteBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS,
},
defaultRootObject: 'index.html',
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: cdk.Duration.seconds(0),
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: cdk.Duration.seconds(0),
},
],
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
});
new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
new cdk.CfnOutput(this, 'DistributionDomainName', { value: distribution.distributionDomainName });
new cdk.CfnOutput(this, 'SiteBucketName', { value: siteBucket.bucketName });
}
}