Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/mesh-v2-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as cdk from 'aws-cdk-lib/core';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
Expand Down Expand Up @@ -131,6 +132,62 @@ export class MeshV2Stack extends cdk.Stack {
},
});

// WAF configuration (Only for production)
if (stage === 'prod') {
const allowedOrigins = [
'https://smalruby.app',
'https://smalruby.jp'
];

const webAcl = new wafv2.CfnWebACL(this, 'MeshV2ApiWebAcl', {
defaultAction: { block: {} },
scope: 'REGIONAL',
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'MeshV2ApiWebAcl',
sampledRequestsEnabled: true,
},
rules: [
{
name: 'AllowSpecificOrigins',
priority: 1,
action: { allow: {} },
statement: {
orStatement: {
statements: allowedOrigins.map(origin => ({
byteMatchStatement: {
fieldToMatch: {
singleHeader: {
name: 'origin',
},
},
positionalConstraint: 'EXACTLY',
searchString: origin,
textTransformations: [
{
priority: 0,
type: 'LOWERCASE',
},
],
},
})),
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AllowSpecificOrigins',
sampledRequestsEnabled: true,
},
},
],
});

new wafv2.CfnWebACLAssociation(this, 'MeshV2ApiWebAclAssociation', {
resourceArn: this.api.arn,
webAclArn: webAcl.attrArn,
});
}

// Route53 Alias record for Custom Domain
if (customDomain && zone) {
// Extract subdomain from customDomain (e.g., "graphql.api.smalruby.app" -> "graphql")
Expand Down
114 changes: 98 additions & 16 deletions test/mesh-v2.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,99 @@
// import * as cdk from 'aws-cdk-lib/core';
// import { Template } from 'aws-cdk-lib/assertions';
// import * as MeshV2 from '../lib/mesh-v2-stack';

// example test. To run these tests, uncomment this file along with the
// example resource in lib/mesh-v2-stack.ts
test('SQS Queue Created', () => {
// const app = new cdk.App();
// // WHEN
// const stack = new MeshV2.MeshV2Stack(app, 'MyTestStack');
// // THEN
// const template = Template.fromStack(stack);

// template.hasResourceProperties('AWS::SQS::Queue', {
// VisibilityTimeout: 300
// });
import * as cdk from 'aws-cdk-lib/core';
import { Template } from 'aws-cdk-lib/assertions';
import * as MeshV2 from '../lib/mesh-v2-stack';

describe('MeshV2Stack', () => {
test('AppSync API and DynamoDB Table Created', () => {
const app = new cdk.App();
const stack = new MeshV2.MeshV2Stack(app, 'MyTestStack', {
env: { account: '123456789012', region: 'us-east-1' }
});
const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::DynamoDB::Table', {
TableName: 'MeshV2Table-stg'
});

template.hasResourceProperties('AWS::AppSync::GraphQLApi', {
Name: 'MeshV2Api-stg'
});
});

test('WAF is created when stage is prod', () => {
const app = new cdk.App({
context: {
stage: 'prod'
}
});
const stack = new MeshV2.MeshV2Stack(app, 'MyProdTestStack', {
env: { account: '123456789012', region: 'us-east-1' }
});
const template = Template.fromStack(stack);

template.resourceCountIs('AWS::WAFv2::WebACL', 1);
template.hasResourceProperties('AWS::WAFv2::WebACL', {
DefaultAction: { Block: {} },
Scope: 'REGIONAL',
Rules: [
{
Name: 'AllowSpecificOrigins',
Priority: 1,
Action: { Allow: {} },
Statement: {
OrStatement: {
Statements: [
{
ByteMatchStatement: {
FieldToMatch: {
SingleHeader: { name: 'origin' }
},
PositionalConstraint: 'EXACTLY',
SearchString: 'https://smalruby.app',
TextTransformations: [
{
Priority: 0,
Type: 'LOWERCASE'
}
]
}
},
{
ByteMatchStatement: {
FieldToMatch: {
SingleHeader: { name: 'origin' }
},
PositionalConstraint: 'EXACTLY',
SearchString: 'https://smalruby.jp',
TextTransformations: [
{
Priority: 0,
Type: 'LOWERCASE'
}
]
}
}
]
}
}
}
]
});

template.resourceCountIs('AWS::WAFv2::WebACLAssociation', 1);
});

test('WAF is not created when stage is stg', () => {
const app = new cdk.App({
context: {
stage: 'stg'
}
});
const stack = new MeshV2.MeshV2Stack(app, 'MyStgTestStack', {
env: { account: '123456789012', region: 'us-east-1' }
});
const template = Template.fromStack(stack);

template.resourceCountIs('AWS::WAFv2::WebACL', 0);
template.resourceCountIs('AWS::WAFv2::WebACLAssociation', 0);
});
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"skipLibCheck": true,
"isolatedModules": true,
"typeRoots": [
"./node_modules/@types"
]
Expand Down
Loading