diff --git a/apps/aws-app/cdk/app.ts b/apps/aws-app/cdk/app.ts new file mode 100644 index 0000000..cd534fb --- /dev/null +++ b/apps/aws-app/cdk/app.ts @@ -0,0 +1,24 @@ +import * as cdk from 'aws-cdk-lib'; +import {MainStack} from './main-stack.js'; +import {WafStack} from './waf-stack.js'; + +const app = new cdk.App(); + +const wafStack = new WafStack(app, `mfng-waf`, { + crossRegionReferences: true, + env: { + // For a WAF with CLOUDFRONT scope, the WAF resources must be created in the + // US East (N. Virginia) Region, us-east-1. + region: 'us-east-1', + }, +}); + +new MainStack(app, `mfng-app`, { + crossRegionReferences: true, + env: { + // Cross stack/region references are only supported for stacks with an + // explicit region defined. + region: process.env.AWS_REGION, + }, + webAcl: wafStack.webAcl, +}); diff --git a/apps/aws-app/cdk/stack.ts b/apps/aws-app/cdk/main-stack.ts similarity index 69% rename from apps/aws-app/cdk/stack.ts rename to apps/aws-app/cdk/main-stack.ts index 380625f..172ffd5 100644 --- a/apps/aws-app/cdk/stack.ts +++ b/apps/aws-app/cdk/main-stack.ts @@ -5,9 +5,17 @@ import type {Construct} from 'constructs'; const verifyHeader = process.env.AWS_HANDLER_VERIFY_HEADER; const distDirname = path.join(import.meta.dirname, `../dist/`); -export class Stack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); +export interface MainStackProps extends cdk.StackProps { + readonly webAcl: cdk.aws_wafv2.CfnWebACL; +} + +export class MainStack extends cdk.Stack { + #webAcl: cdk.aws_wafv2.CfnWebACL; + + constructor(scope: Construct, id: string, props: MainStackProps) { + const {webAcl, ...otherProps} = props; + super(scope, id, otherProps); + this.#webAcl = webAcl; const lambdaFunction = new cdk.aws_lambda_nodejs.NodejsFunction( this, @@ -23,15 +31,11 @@ export class Stack extends cdk.Stack { }, ); - const lambdaFunctionUrl = new cdk.aws_lambda.FunctionUrl( - this, - `function-url`, - { - function: lambdaFunction, - authType: cdk.aws_lambda.FunctionUrlAuthType.NONE, - invokeMode: cdk.aws_lambda.InvokeMode.RESPONSE_STREAM, - }, - ); + const functionUrl = new cdk.aws_lambda.FunctionUrl(this, `function-url`, { + function: lambdaFunction, + authType: cdk.aws_lambda.FunctionUrlAuthType.NONE, + invokeMode: cdk.aws_lambda.InvokeMode.RESPONSE_STREAM, + }); const bucket = new cdk.aws_s3.Bucket(this, `assets-bucket`, { bucketName: `mfng-aws-app-assets`, @@ -41,14 +45,11 @@ export class Stack extends cdk.Stack { const distribution = new cdk.aws_cloudfront.Distribution(this, `cdn`, { defaultBehavior: { - origin: new cdk.aws_cloudfront_origins.FunctionUrlOrigin( - lambdaFunctionUrl, - { - customHeaders: verifyHeader - ? {'X-Origin-Verify': verifyHeader} - : undefined, - }, - ), + origin: new cdk.aws_cloudfront_origins.FunctionUrlOrigin(functionUrl, { + customHeaders: verifyHeader + ? {'X-Origin-Verify': verifyHeader} + : undefined, + }), allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: new cdk.aws_cloudfront.CachePolicy(this, `cache-policy`, { enableAcceptEncodingGzip: true, @@ -68,6 +69,7 @@ export class Stack extends cdk.Stack { }, }, priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_100, + webAclId: this.#webAcl.attrArn, }); new cdk.aws_s3_deployment.BucketDeployment(this, `assets-deployment`, { @@ -82,12 +84,14 @@ export class Stack extends cdk.Stack { cacheControl: [cdk.aws_s3_deployment.CacheControl.immutable()], }); - new cdk.CfnOutput(this, `cdn-domain-name`, { - value: distribution.domainName, + new cdk.CfnOutput(this, `function-url-output`, { + exportName: `function-url`, + value: functionUrl.url, + }); + + new cdk.CfnOutput(this, `cdn-url-output`, { + exportName: `cdn-url`, + value: `https://${distribution.domainName}`, }); } } - -const app = new cdk.App(); - -new Stack(app, `mfng-aws-app`); diff --git a/apps/aws-app/cdk/waf-stack.ts b/apps/aws-app/cdk/waf-stack.ts new file mode 100644 index 0000000..341298a --- /dev/null +++ b/apps/aws-app/cdk/waf-stack.ts @@ -0,0 +1,27 @@ +import * as cdk from 'aws-cdk-lib'; +import type {Construct} from 'constructs'; + +export class WafStack extends cdk.Stack { + #webAcl: cdk.aws_wafv2.CfnWebACL; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + this.#webAcl = new cdk.aws_wafv2.CfnWebACL(this, `waf`, { + name: `mfng-waf`, + scope: `CLOUDFRONT`, + defaultAction: { + allow: {}, + }, + visibilityConfig: { + cloudWatchMetricsEnabled: false, + metricName: `mfng-waf-metric`, + sampledRequestsEnabled: true, + }, + }); + } + + get webAcl() { + return this.#webAcl; + } +} diff --git a/apps/aws-app/package.json b/apps/aws-app/package.json index 119457a..129ecdf 100644 --- a/apps/aws-app/package.json +++ b/apps/aws-app/package.json @@ -8,8 +8,8 @@ "scripts": { "build": "NODE_OPTIONS='--import=tsx --conditions=@mfng:internal' webpack --mode production", "build:dev": "NODE_OPTIONS='--import=tsx --conditions=@mfng:internal' webpack --mode development", - "predeploy": "cdk bootstrap --app 'tsx cdk/stack.ts'", - "deploy": "cdk diff --app 'tsx cdk/stack.ts' && cdk deploy --app 'tsx cdk/stack.ts'", + "predeploy": "cdk bootstrap --app 'tsx cdk/app.ts'", + "deploy": "cdk diff --app 'tsx cdk/app.ts' && cdk deploy --app 'tsx cdk/app.ts' --all", "dev": "npm start", "start": "tsx watch --clear-screen=false --enable-source-maps --inspect dev-server/run.ts", "watch": "NODE_OPTIONS='--import=tsx --conditions=@mfng:internal' webpack --mode production --watch",