@bitblit/ratchet-epsilon-deployment 6.0.145-alpha → 6.0.147-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/build/ratchet-epsilon-deployment-info.ts +19 -0
- package/src/deployment/cdk/bucket-and-behavior-options.ts +7 -0
- package/src/deployment/cdk/epsilon-api-stack-feature.ts +5 -0
- package/src/deployment/cdk/epsilon-api-stack-props.ts +32 -0
- package/src/deployment/cdk/epsilon-api-stack.ts +284 -0
- package/src/deployment/cdk/epsilon-lambda-to-cloudfront-path-mapping.ts +9 -0
- package/src/deployment/cdk/epsilon-route-53-handling.ts +7 -0
- package/src/deployment/cdk/epsilon-simple-lambda-cloudfront-distribution-props.ts +25 -0
- package/src/deployment/cdk/epsilon-simple-lambda-cloudfront-distribution.ts +48 -0
- package/src/deployment/cdk/epsilon-stack-util.spec.ts +10 -0
- package/src/deployment/cdk/epsilon-stack-util.ts +164 -0
- package/src/deployment/cdk/epsilon-website-cache-behavior.ts +5 -0
- package/src/deployment/cdk/epsilon-website-stack-props.ts +19 -0
- package/src/deployment/cdk/epsilon-website-stack.ts +148 -0
- package/src/deployment/cdk/simple-additional-s3-website-mapping.ts +4 -0
- package/src/deployment/index.ts +14 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitblit/ratchet-epsilon-deployment",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.147-alpha",
|
|
4
4
|
"description": "Epsilon CDK extensions to simplify deployment",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
|
+
"src/**",
|
|
8
9
|
"lib/**",
|
|
9
10
|
"bin/**"
|
|
10
11
|
],
|
|
@@ -50,14 +51,14 @@
|
|
|
50
51
|
},
|
|
51
52
|
"license": "Apache-2.0",
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@bitblit/ratchet-aws": "6.0.
|
|
54
|
-
"@bitblit/ratchet-common": "6.0.
|
|
55
|
-
"@bitblit/ratchet-epsilon-common": "6.0.
|
|
54
|
+
"@bitblit/ratchet-aws": "6.0.147-alpha",
|
|
55
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
56
|
+
"@bitblit/ratchet-epsilon-common": "6.0.147-alpha",
|
|
56
57
|
"aws-cdk-lib": "2.221.1",
|
|
57
58
|
"constructs": "10.4.2"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
|
-
"@bitblit/ratchet-common": "6.0.
|
|
61
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
61
62
|
"aws-cdk-lib": "^2.221.1",
|
|
62
63
|
"constructs": "^10.4.2"
|
|
63
64
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BuildInformation } from '@bitblit/ratchet-common/build/build-information';
|
|
2
|
+
|
|
3
|
+
export class RatchetEpsilonDeploymentInfo {
|
|
4
|
+
// Empty constructor prevents instantiation
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
6
|
+
private constructor() {}
|
|
7
|
+
|
|
8
|
+
public static buildInformation(): BuildInformation {
|
|
9
|
+
const val: BuildInformation = {
|
|
10
|
+
version: 'LOCAL-SNAPSHOT',
|
|
11
|
+
hash: 'LOCAL-HASH',
|
|
12
|
+
branch: 'LOCAL-BRANCH',
|
|
13
|
+
tag: 'LOCAL-TAG',
|
|
14
|
+
timeBuiltISO: 'LOCAL-TIME-ISO',
|
|
15
|
+
notes: 'LOCAL-NOTES',
|
|
16
|
+
};
|
|
17
|
+
return val;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StackProps } from 'aws-cdk-lib';
|
|
2
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
3
|
+
import { EpsilonApiStackFeature } from './epsilon-api-stack-feature.js';
|
|
4
|
+
import { SubnetAttributes } from 'aws-cdk-lib/aws-ec2';
|
|
5
|
+
import { EpsilonSimpleLambdaCloudfrontDistributionProps } from './epsilon-simple-lambda-cloudfront-distribution-props';
|
|
6
|
+
|
|
7
|
+
export interface EpsilonApiStackProps extends StackProps {
|
|
8
|
+
batchInstancesEc2KeyPairName?: string;
|
|
9
|
+
additionalPolicyStatements: PolicyStatement[];
|
|
10
|
+
|
|
11
|
+
disabledFeatures?: EpsilonApiStackFeature[];
|
|
12
|
+
|
|
13
|
+
dockerFileFolder: string;
|
|
14
|
+
dockerFileName: string;
|
|
15
|
+
|
|
16
|
+
lambdaSecurityGroupIds: string[];
|
|
17
|
+
vpcSubnetAttributes: SubnetAttributes[];
|
|
18
|
+
vpcId: string;
|
|
19
|
+
|
|
20
|
+
extraEnvironmentalVars?: Record<string, string>;
|
|
21
|
+
webLambdaPingMinutes?: number;
|
|
22
|
+
|
|
23
|
+
webMemorySizeMb?: number;
|
|
24
|
+
backgroundMemorySizeMb?: number;
|
|
25
|
+
|
|
26
|
+
webTimeoutSeconds?: number;
|
|
27
|
+
backgroundTimeoutSeconds?: number;
|
|
28
|
+
|
|
29
|
+
replaceBatchComputeEnvironment?: boolean;
|
|
30
|
+
|
|
31
|
+
autoCloudfrontDistribution?: EpsilonSimpleLambdaCloudfrontDistributionProps;
|
|
32
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Duration, Lazy, Size, Stack } from 'aws-cdk-lib';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { DockerImageCode, DockerImageFunction, FunctionUrl, FunctionUrlAuthType, HttpMethod } from 'aws-cdk-lib/aws-lambda';
|
|
4
|
+
import { ManagedPolicy, PolicyDocument, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
5
|
+
import { Topic } from 'aws-cdk-lib/aws-sns';
|
|
6
|
+
import { Queue } from 'aws-cdk-lib/aws-sqs';
|
|
7
|
+
import { LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
|
|
8
|
+
|
|
9
|
+
import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
|
|
10
|
+
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
|
|
11
|
+
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
|
|
12
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
13
|
+
import { EpsilonStackUtil } from './epsilon-stack-util.js';
|
|
14
|
+
import { EpsilonApiStackProps } from './epsilon-api-stack-props.js';
|
|
15
|
+
import { RatchetEpsilonDeploymentInfo } from '../../build/ratchet-epsilon-deployment-info.js';
|
|
16
|
+
import {
|
|
17
|
+
EcsFargateContainerDefinition,
|
|
18
|
+
EcsFargateContainerDefinitionProps,
|
|
19
|
+
EcsJobDefinition,
|
|
20
|
+
EcsJobDefinitionProps,
|
|
21
|
+
FargateComputeEnvironment,
|
|
22
|
+
FargateComputeEnvironmentProps,
|
|
23
|
+
JobQueue,
|
|
24
|
+
JobQueueProps,
|
|
25
|
+
} from 'aws-cdk-lib/aws-batch';
|
|
26
|
+
import { SecurityGroup, Subnet, SubnetSelection, Vpc } from 'aws-cdk-lib/aws-ec2';
|
|
27
|
+
|
|
28
|
+
import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
|
|
29
|
+
import { EpsilonApiStackFeature } from './epsilon-api-stack-feature.js';
|
|
30
|
+
import { EpsilonSimpleLambdaCloudfrontDistributionProps } from './epsilon-simple-lambda-cloudfront-distribution-props';
|
|
31
|
+
import { EpsilonSimpleLambdaCloudfrontDistribution } from './epsilon-simple-lambda-cloudfront-distribution';
|
|
32
|
+
import { EpsilonRoute53Handling } from './epsilon-route-53-handling';
|
|
33
|
+
import { HostedZone, RecordSet, RecordType } from 'aws-cdk-lib/aws-route53';
|
|
34
|
+
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
|
|
35
|
+
|
|
36
|
+
export class EpsilonApiStack extends Stack {
|
|
37
|
+
private webHandler: DockerImageFunction;
|
|
38
|
+
private backgroundHandler: DockerImageFunction;
|
|
39
|
+
|
|
40
|
+
public webFunctionUrl: FunctionUrl;
|
|
41
|
+
public apiDomain: string;
|
|
42
|
+
|
|
43
|
+
constructor(scope: Construct, id: string, props?: EpsilonApiStackProps) {
|
|
44
|
+
super(scope, id, props);
|
|
45
|
+
|
|
46
|
+
const disabledFeatures: EpsilonApiStackFeature[] = props?.disabledFeatures || [];
|
|
47
|
+
|
|
48
|
+
// Build the docker image first
|
|
49
|
+
const dockerImageAsset: DockerImageAsset = new DockerImageAsset(this, id + 'DockerImage', {
|
|
50
|
+
directory: props.dockerFileFolder,
|
|
51
|
+
file: props.dockerFileName,
|
|
52
|
+
});
|
|
53
|
+
const dockerImageCode: DockerImageCode = DockerImageCode.fromImageAsset(props.dockerFileFolder, { file: props.dockerFileName });
|
|
54
|
+
|
|
55
|
+
const notificationTopic: Topic = new Topic(this, id + 'WorkNotificationTopic');
|
|
56
|
+
const workQueue: Queue = new Queue(this, id + 'WorkQueue', {
|
|
57
|
+
fifo: true,
|
|
58
|
+
retentionPeriod: Duration.hours(8),
|
|
59
|
+
visibilityTimeout: Duration.minutes(5),
|
|
60
|
+
contentBasedDeduplication: true,
|
|
61
|
+
...props,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const interApiGenericEventTopic: Topic = new Topic(this, id + 'InterApiTopic');
|
|
65
|
+
|
|
66
|
+
const epsilonEnv: Record<string, string> = {
|
|
67
|
+
EPSILON_AWS_REGION: StringRatchet.safeString(Stack.of(this).region),
|
|
68
|
+
EPSILON_AWS_AVAILABILITY_ZONES: StringRatchet.safeString(JSON.stringify(Stack.of(this).availabilityZones)),
|
|
69
|
+
EPSILON_BACKGROUND_SQS_QUEUE_URL: StringRatchet.safeString(workQueue.queueUrl),
|
|
70
|
+
EPSILON_BACKGROUND_SNS_TOPIC_ARN: StringRatchet.safeString(notificationTopic.topicArn),
|
|
71
|
+
EPSILON_INTER_API_EVENT_TOPIC_ARN: StringRatchet.safeString(interApiGenericEventTopic.topicArn),
|
|
72
|
+
EPSILON_LIB_BUILD_HASH: StringRatchet.safeString(RatchetEpsilonDeploymentInfo.buildInformation().hash),
|
|
73
|
+
EPSILON_LIB_BUILD_TIME: StringRatchet.safeString(RatchetEpsilonDeploymentInfo.buildInformation().timeBuiltISO),
|
|
74
|
+
EPSILON_LIB_BUILD_BRANCH_OR_TAG: StringRatchet.safeString(
|
|
75
|
+
RatchetEpsilonDeploymentInfo.buildInformation().branch || RatchetEpsilonDeploymentInfo.buildInformation().tag,
|
|
76
|
+
),
|
|
77
|
+
EPSILON_LIB_BUILD_VERSION: StringRatchet.safeString(RatchetEpsilonDeploymentInfo.buildInformation().version),
|
|
78
|
+
};
|
|
79
|
+
const env: Record<string, string> = Object.assign({}, props.extraEnvironmentalVars || {}, epsilonEnv);
|
|
80
|
+
|
|
81
|
+
if (!disabledFeatures.includes(EpsilonApiStackFeature.AwsBatchHandler)) {
|
|
82
|
+
// Then build the Batch compute stuff...
|
|
83
|
+
// This is the role that ECS uses to pull containers, secrets, etc
|
|
84
|
+
const executionRole = new Role(this, id + 'BatchExecutionRole', {
|
|
85
|
+
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), //'ec2.amazonaws.com'),
|
|
86
|
+
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')],
|
|
87
|
+
inlinePolicies: {
|
|
88
|
+
root: new PolicyDocument({
|
|
89
|
+
statements: EpsilonStackUtil.ECS_POLICY_STATEMENTS,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// This is the role used by the container to actually do business logic (your code uses this role)
|
|
95
|
+
const jobRole = new Role(this, id + 'BatchJobRole', {
|
|
96
|
+
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
97
|
+
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')],
|
|
98
|
+
inlinePolicies: {
|
|
99
|
+
root: new PolicyDocument({
|
|
100
|
+
statements: EpsilonStackUtil.createDefaultPolicyStatementList(props, workQueue, notificationTopic, interApiGenericEventTopic),
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const subnetSelection: SubnetSelection = {
|
|
106
|
+
subnets: props.vpcSubnetAttributes.map((subnetAttr, index) => Subnet.fromSubnetAttributes(this, `VpcSubnet${index}`, subnetAttr)),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Created AWSServiceBatchRole
|
|
110
|
+
// https://docs.aws.amazon.com/batch/latest/userguide/service_IAM_role.html
|
|
111
|
+
const compEnvProps: FargateComputeEnvironmentProps = {
|
|
112
|
+
vpc: Vpc.fromLookup(this, `Vpc`, { vpcId: props.vpcId }),
|
|
113
|
+
computeEnvironmentName: id + 'ComputeEnv',
|
|
114
|
+
enabled: true,
|
|
115
|
+
maxvCpus: 16,
|
|
116
|
+
replaceComputeEnvironment: props.replaceBatchComputeEnvironment ?? false,
|
|
117
|
+
securityGroups: props.lambdaSecurityGroupIds.map((sgId, index) =>
|
|
118
|
+
SecurityGroup.fromSecurityGroupId(this, `SecurityGroup${index}`, `sg-${sgId}`),
|
|
119
|
+
),
|
|
120
|
+
serviceRole: Role.fromRoleArn(
|
|
121
|
+
this,
|
|
122
|
+
`${id}ServiceRole`,
|
|
123
|
+
'arn:aws:iam::' + props.env.account + ':role/aws-service-role/batch.amazonaws.com/AWSServiceRoleForBatch',
|
|
124
|
+
),
|
|
125
|
+
//Role.fromRoleArn(this, `${id}ServiceRole`, 'arn:aws:iam::' + props.env.account + ':role/AWSBatchServiceRole'),
|
|
126
|
+
spot: false,
|
|
127
|
+
terminateOnUpdate: false,
|
|
128
|
+
updateTimeout: Duration.hours(4),
|
|
129
|
+
updateToLatestImageVersion: true,
|
|
130
|
+
vpcSubnets: subnetSelection,
|
|
131
|
+
//vpcSubnets: subnetSelection,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const compEnv: FargateComputeEnvironment = new FargateComputeEnvironment(this, id + 'ComputeEnv', compEnvProps);
|
|
135
|
+
|
|
136
|
+
const batchJobQueueProps: JobQueueProps = {
|
|
137
|
+
computeEnvironments: [{ order: 1, computeEnvironment: compEnv }],
|
|
138
|
+
enabled: true,
|
|
139
|
+
jobQueueName: id + 'BatchJobQueue',
|
|
140
|
+
priority: 10,
|
|
141
|
+
schedulingPolicy: undefined, // Implement later?
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const batchJobQueue = new JobQueue(this, id + 'BatchJobQueue', batchJobQueueProps);
|
|
145
|
+
|
|
146
|
+
const batchEnvVars: Record<string, any> = EpsilonStackUtil.toEnvironmentVariables([
|
|
147
|
+
env,
|
|
148
|
+
props.extraEnvironmentalVars || {},
|
|
149
|
+
{
|
|
150
|
+
EPSILON_RUNNING_IN_AWS_BATCH: true,
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const containerDef: EcsFargateContainerDefinitionProps = {
|
|
155
|
+
cpu: 4,
|
|
156
|
+
image: ContainerImage.fromRegistry(dockerImageAsset.imageUri),
|
|
157
|
+
memory: Size.mebibytes(8192),
|
|
158
|
+
assignPublicIp: true, // Need this to talk to ECS to get the container
|
|
159
|
+
command: ['Ref::taskName', 'Ref::taskDataBase64', 'Ref::traceId', 'Ref::traceDepth', 'Ref::taskMetaDataBase64'], // Bootstrap to the Lambda handler
|
|
160
|
+
environment: batchEnvVars,
|
|
161
|
+
executionRole: executionRole,
|
|
162
|
+
//fargatePlatformVersion: undefined,
|
|
163
|
+
jobRole: jobRole, //Role.fromRoleArn(this, `${id}JobRole`, jobRole.roleArn),
|
|
164
|
+
//linuxParameters: undefined,
|
|
165
|
+
readonlyRootFilesystem: false,
|
|
166
|
+
//secrets: undefined,
|
|
167
|
+
//user: undefined,
|
|
168
|
+
volumes: [],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const fargateContainerDefinitionDef = new EcsFargateContainerDefinition(this, `${id}FargateContainerDefinition`, containerDef);
|
|
172
|
+
|
|
173
|
+
const jobProps: EcsJobDefinitionProps = {
|
|
174
|
+
jobDefinitionName: id + 'JobDefinition',
|
|
175
|
+
retryAttempts: 3,
|
|
176
|
+
retryStrategies: undefined,
|
|
177
|
+
schedulingPriority: undefined,
|
|
178
|
+
timeout: undefined,
|
|
179
|
+
container: fargateContainerDefinitionDef,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const jobDef: EcsJobDefinition = new EcsJobDefinition(this, id + 'JobDefinition', jobProps);
|
|
183
|
+
|
|
184
|
+
// Add AWS batch vars to the environment
|
|
185
|
+
env['EPSILON_AWS_BATCH_JOB_DEFINITION_ARN'] = jobDef.jobDefinitionArn; // .ref;
|
|
186
|
+
env['EPSILON_AWS_BATCH_JOB_QUEUE_ARN'] = batchJobQueue.jobQueueArn; // .ref;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// This is needed for both background and web lambdas
|
|
190
|
+
const lambdaRole = new Role(this, 'customRole', {
|
|
191
|
+
roleName: id + 'LambdaCustomRole',
|
|
192
|
+
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
|
|
193
|
+
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')],
|
|
194
|
+
inlinePolicies: {
|
|
195
|
+
root: new PolicyDocument({
|
|
196
|
+
statements: EpsilonStackUtil.createDefaultPolicyStatementList(props, workQueue, notificationTopic, interApiGenericEventTopic),
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!disabledFeatures.includes(EpsilonApiStackFeature.WebLambda)) {
|
|
202
|
+
this.webHandler = new DockerImageFunction(this, id + 'Web', {
|
|
203
|
+
//reservedConcurrentExecutions: 1,
|
|
204
|
+
retryAttempts: 2,
|
|
205
|
+
//allowAllOutbound: true, // Needs a VPC
|
|
206
|
+
memorySize: props.webMemorySizeMb || 128,
|
|
207
|
+
ephemeralStorageSize: Size.mebibytes(512),
|
|
208
|
+
timeout: Duration.seconds(props.webTimeoutSeconds || 20),
|
|
209
|
+
code: dockerImageCode,
|
|
210
|
+
role: lambdaRole,
|
|
211
|
+
environment: env,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (props?.webLambdaPingMinutes && props.webLambdaPingMinutes > 0) {
|
|
215
|
+
// Wire up the cron handler
|
|
216
|
+
const rule = new Rule(this, id + 'WebKeepaliveRule', {
|
|
217
|
+
schedule: Schedule.rate(Duration.minutes(Math.ceil(props.webLambdaPingMinutes))),
|
|
218
|
+
});
|
|
219
|
+
rule.addTarget(new LambdaFunction(this.webHandler));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.webFunctionUrl = this.webHandler.addFunctionUrl({
|
|
223
|
+
authType: FunctionUrlAuthType.NONE,
|
|
224
|
+
cors: {
|
|
225
|
+
allowedOrigins: ['*'],
|
|
226
|
+
allowedHeaders: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key'],
|
|
227
|
+
allowedMethods: [HttpMethod.ALL],
|
|
228
|
+
allowCredentials: true,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
this.apiDomain = Lazy.uncachedString({
|
|
233
|
+
produce: (context) => {
|
|
234
|
+
const resolved = context.resolve(this.webFunctionUrl.url);
|
|
235
|
+
return { 'Fn::Select': [2, { 'Fn::Split': ['/', resolved] }] } as any;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (props.autoCloudfrontDistribution) {
|
|
240
|
+
const distroPropsCopy: EpsilonSimpleLambdaCloudfrontDistributionProps = Object.assign({}, props.autoCloudfrontDistribution);
|
|
241
|
+
distroPropsCopy.lambdaFunctionDomain = this.webFunctionUrl;
|
|
242
|
+
const dist = new EpsilonSimpleLambdaCloudfrontDistribution(this, id + 'DirectApiCloudfrontDistro', distroPropsCopy);
|
|
243
|
+
// Have to be able to skip this since SOME people don't do DNS in Route53
|
|
244
|
+
if (props?.autoCloudfrontDistribution.route53Handling === EpsilonRoute53Handling.Update) {
|
|
245
|
+
if (props?.autoCloudfrontDistribution.domainNames?.length) {
|
|
246
|
+
for (const dn of props.autoCloudfrontDistribution.domainNames) {
|
|
247
|
+
const _domain: RecordSet = new RecordSet(this, id + 'DomainName-' + dn, {
|
|
248
|
+
recordType: RecordType.A,
|
|
249
|
+
recordName: dn,
|
|
250
|
+
target: {
|
|
251
|
+
aliasTarget: new CloudFrontTarget(dist),
|
|
252
|
+
},
|
|
253
|
+
zone: HostedZone.fromLookup(this, id + 'HostZone-' + dn, { domainName: EpsilonStackUtil.extractApexDomain(dn) }),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!disabledFeatures.includes(EpsilonApiStackFeature.BackgroundLambda)) {
|
|
262
|
+
this.backgroundHandler = new DockerImageFunction(this, id + 'Background', {
|
|
263
|
+
//reservedConcurrentExecutions: 1,
|
|
264
|
+
retryAttempts: 2,
|
|
265
|
+
// allowAllOutbound: true,
|
|
266
|
+
memorySize: props.backgroundMemorySizeMb || 3000,
|
|
267
|
+
ephemeralStorageSize: Size.mebibytes(512),
|
|
268
|
+
timeout: Duration.seconds(props.backgroundTimeoutSeconds || 900),
|
|
269
|
+
code: dockerImageCode,
|
|
270
|
+
role: lambdaRole,
|
|
271
|
+
environment: env,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
notificationTopic.addSubscription(new LambdaSubscription(this.backgroundHandler));
|
|
275
|
+
interApiGenericEventTopic.addSubscription(new LambdaSubscription(this.backgroundHandler));
|
|
276
|
+
|
|
277
|
+
// Wire up the cron handler
|
|
278
|
+
const rule = new Rule(this, id + 'CronRule', {
|
|
279
|
+
schedule: Schedule.rate(Duration.minutes(1)),
|
|
280
|
+
});
|
|
281
|
+
rule.addTarget(new LambdaFunction(this.backgroundHandler));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FunctionUrl } from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { IResponseHeadersPolicy } from 'aws-cdk-lib/aws-cloudfront';
|
|
4
|
+
|
|
5
|
+
export interface EpsilonLambdaToCloudfrontPathMapping {
|
|
6
|
+
lambdaFunctionUrl: FunctionUrl;
|
|
7
|
+
pathPattern: string;
|
|
8
|
+
responseHeadersPolicyCreator?: (scope: Construct, id: string) => IResponseHeadersPolicy;
|
|
9
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// NOTE: This is a psuedo-enum to fix some issues with Typescript enums. See: https://exploringjs.com/tackling-ts/ch_enum-alternatives.html for details
|
|
2
|
+
|
|
3
|
+
export const EpsilonRoute53Handling = {
|
|
4
|
+
Update: 'Update',
|
|
5
|
+
DoNotUpdate: 'DoNotUpdate',
|
|
6
|
+
};
|
|
7
|
+
export type EpsilonRoute53Handling = (typeof EpsilonRoute53Handling)[keyof typeof EpsilonRoute53Handling];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StackProps } from 'aws-cdk-lib';
|
|
2
|
+
import {
|
|
3
|
+
AllowedMethods,
|
|
4
|
+
ICachePolicy,
|
|
5
|
+
IResponseHeadersPolicy,
|
|
6
|
+
PriceClass,
|
|
7
|
+
SSLMethod,
|
|
8
|
+
ViewerProtocolPolicy,
|
|
9
|
+
} from 'aws-cdk-lib/aws-cloudfront';
|
|
10
|
+
import { FunctionUrl } from 'aws-cdk-lib/aws-lambda';
|
|
11
|
+
import { EpsilonRoute53Handling } from './epsilon-route-53-handling';
|
|
12
|
+
import { Construct } from 'constructs';
|
|
13
|
+
|
|
14
|
+
export interface EpsilonSimpleLambdaCloudfrontDistributionProps extends StackProps {
|
|
15
|
+
lambdaFunctionDomain: FunctionUrl;
|
|
16
|
+
httpsCertArn: string;
|
|
17
|
+
domainNames: string[];
|
|
18
|
+
protocolPolicy?: ViewerProtocolPolicy;
|
|
19
|
+
cachePolicy?: ICachePolicy;
|
|
20
|
+
priceClass?: PriceClass;
|
|
21
|
+
sslMethod?: SSLMethod;
|
|
22
|
+
route53Handling?: EpsilonRoute53Handling;
|
|
23
|
+
allowedMethods?: AllowedMethods;
|
|
24
|
+
responseHeadersPolicyCreator?: (scope: Construct, id: string) => IResponseHeadersPolicy;
|
|
25
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import {
|
|
3
|
+
AllowedMethods,
|
|
4
|
+
BehaviorOptions,
|
|
5
|
+
CachePolicy,
|
|
6
|
+
Distribution,
|
|
7
|
+
DistributionProps,
|
|
8
|
+
IResponseHeadersPolicy,
|
|
9
|
+
OriginRequestPolicy,
|
|
10
|
+
PriceClass,
|
|
11
|
+
ResponseHeadersPolicy,
|
|
12
|
+
SSLMethod,
|
|
13
|
+
ViewerProtocolPolicy,
|
|
14
|
+
} from 'aws-cdk-lib/aws-cloudfront';
|
|
15
|
+
import { FunctionUrlOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
16
|
+
import { EpsilonSimpleLambdaCloudfrontDistributionProps } from './epsilon-simple-lambda-cloudfront-distribution-props.js';
|
|
17
|
+
import { Certificate, ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
18
|
+
|
|
19
|
+
export class EpsilonSimpleLambdaCloudfrontDistribution extends Distribution {
|
|
20
|
+
constructor(scope: Construct, id: string, props?: EpsilonSimpleLambdaCloudfrontDistributionProps) {
|
|
21
|
+
let policy: IResponseHeadersPolicy = ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS;
|
|
22
|
+
if (props.responseHeadersPolicyCreator) {
|
|
23
|
+
policy = props.responseHeadersPolicyCreator(scope, id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const behavior: BehaviorOptions = {
|
|
27
|
+
origin: new FunctionUrlOrigin(props.lambdaFunctionDomain),
|
|
28
|
+
compress: true,
|
|
29
|
+
viewerProtocolPolicy: props.protocolPolicy ?? ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
30
|
+
cachePolicy: props.cachePolicy ?? CachePolicy.CACHING_DISABLED,
|
|
31
|
+
allowedMethods: props.allowedMethods ?? AllowedMethods.ALLOW_ALL,
|
|
32
|
+
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
33
|
+
responseHeadersPolicy: policy,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const httpsCertificate: ICertificate = Certificate.fromCertificateArn(scope, id + 'HttpsCert', props.httpsCertArn);
|
|
37
|
+
|
|
38
|
+
const distributionProps: DistributionProps = {
|
|
39
|
+
defaultBehavior: behavior,
|
|
40
|
+
priceClass: props.priceClass ?? PriceClass.PRICE_CLASS_ALL,
|
|
41
|
+
certificate: httpsCertificate,
|
|
42
|
+
domainNames: props.domainNames,
|
|
43
|
+
sslSupportMethod: props.sslMethod ?? SSLMethod.SNI,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
super(scope, id + 'CloudfrontDistro', distributionProps);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { EpsilonStackUtil } from './epsilon-stack-util.js';
|
|
3
|
+
|
|
4
|
+
describe('#EpsilonStackUtil', function () {
|
|
5
|
+
test('should extract apex domains', async () => {
|
|
6
|
+
expect(EpsilonStackUtil.extractApexDomain('a.b.test.com')).toEqual('test.com');
|
|
7
|
+
expect(EpsilonStackUtil.extractApexDomain('www.test.com')).toEqual('test.com');
|
|
8
|
+
expect(EpsilonStackUtil.extractApexDomain('test.com')).toEqual('test.com');
|
|
9
|
+
}, 500);
|
|
10
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
2
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
3
|
+
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
4
|
+
import { Topic } from 'aws-cdk-lib/aws-sns';
|
|
5
|
+
import { Queue } from 'aws-cdk-lib/aws-sqs';
|
|
6
|
+
import { EpsilonApiStackProps } from './epsilon-api-stack-props.js';
|
|
7
|
+
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
|
|
8
|
+
import { HeadersFrameOption, HeadersReferrerPolicy, ResponseHeadersPolicy } from 'aws-cdk-lib/aws-cloudfront';
|
|
9
|
+
import { Construct } from 'constructs';
|
|
10
|
+
import { Duration } from 'aws-cdk-lib';
|
|
11
|
+
|
|
12
|
+
export class EpsilonStackUtil {
|
|
13
|
+
// Prevent instantiation
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
15
|
+
private constructor() {}
|
|
16
|
+
|
|
17
|
+
public static toEnvironmentVariables(input: Record<string, any>[]): Record<string, string> {
|
|
18
|
+
const rval: Record<string, string> = {};
|
|
19
|
+
input.forEach((inval) => {
|
|
20
|
+
Object.keys(inval).forEach((k) => {
|
|
21
|
+
rval[k] = StringRatchet.safeString(inval[k]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return rval;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static createDefaultPolicyStatementList(
|
|
29
|
+
props: EpsilonApiStackProps,
|
|
30
|
+
backgroundLambdaSqs: Queue,
|
|
31
|
+
backgroundLambdaSns: Topic,
|
|
32
|
+
interApiSns: Topic,
|
|
33
|
+
): PolicyStatement[] {
|
|
34
|
+
const rval: PolicyStatement[] = (props.additionalPolicyStatements || []).concat([
|
|
35
|
+
new PolicyStatement({
|
|
36
|
+
effect: Effect.ALLOW,
|
|
37
|
+
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
|
|
38
|
+
resources: ['arn:aws:logs:*:*:*'],
|
|
39
|
+
}),
|
|
40
|
+
new PolicyStatement({
|
|
41
|
+
effect: Effect.ALLOW,
|
|
42
|
+
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
|
|
43
|
+
resources: ['arn:aws:ses:*'],
|
|
44
|
+
}),
|
|
45
|
+
new PolicyStatement({
|
|
46
|
+
effect: Effect.ALLOW,
|
|
47
|
+
actions: ['sqs:*'],
|
|
48
|
+
resources: [backgroundLambdaSqs.queueArn],
|
|
49
|
+
}),
|
|
50
|
+
new PolicyStatement({
|
|
51
|
+
effect: Effect.ALLOW,
|
|
52
|
+
actions: ['sns:*'],
|
|
53
|
+
resources: [backgroundLambdaSns.topicArn, interApiSns.topicArn],
|
|
54
|
+
}),
|
|
55
|
+
new PolicyStatement({
|
|
56
|
+
effect: Effect.ALLOW,
|
|
57
|
+
actions: ['batch:*'],
|
|
58
|
+
resources: ['*'],
|
|
59
|
+
}),
|
|
60
|
+
new PolicyStatement({
|
|
61
|
+
effect: Effect.ALLOW,
|
|
62
|
+
actions: ['ec2:DescribeSecurityGroups'],
|
|
63
|
+
resources: ['*'],
|
|
64
|
+
}),
|
|
65
|
+
new PolicyStatement({
|
|
66
|
+
effect: Effect.ALLOW,
|
|
67
|
+
actions: ['ec2:DescribeSubnets'],
|
|
68
|
+
resources: ['*'],
|
|
69
|
+
}),
|
|
70
|
+
new PolicyStatement({
|
|
71
|
+
effect: Effect.ALLOW,
|
|
72
|
+
actions: ['ec2:DescribeVpcs'],
|
|
73
|
+
resources: ['*'],
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
Logger.info('Created policy statement list: %j', rval);
|
|
77
|
+
return rval;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public static readonly ALLOW_ECS: PolicyStatement = new PolicyStatement({
|
|
81
|
+
effect: Effect.ALLOW,
|
|
82
|
+
actions: ['ecs:*'],
|
|
83
|
+
resources: ['*'],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
public static readonly ALLOW_ECR: PolicyStatement = new PolicyStatement({
|
|
87
|
+
effect: Effect.ALLOW,
|
|
88
|
+
actions: ['ecr:BatchCheckLayerAvailability', 'ecr:BatchGetImage', 'ecr:GetDownloadUrlForLayer', 'ecr:GetAuthorizationToken'],
|
|
89
|
+
resources: ['*'],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
public static readonly ALLOW_RESTRICTED_LOGS: PolicyStatement = new PolicyStatement({
|
|
93
|
+
effect: Effect.ALLOW,
|
|
94
|
+
actions: ['logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogStreams', 'logs:CreateLogGroup'],
|
|
95
|
+
resources: ['*'],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Used by fargate to read containers, etc
|
|
99
|
+
public static readonly ALLOW_FARGATE_SECRET_READING: PolicyStatement[] = [
|
|
100
|
+
new PolicyStatement({
|
|
101
|
+
effect: Effect.ALLOW,
|
|
102
|
+
actions: ['ssm:GetParameters'],
|
|
103
|
+
resources: ['*'],
|
|
104
|
+
}),
|
|
105
|
+
new PolicyStatement({
|
|
106
|
+
effect: Effect.ALLOW,
|
|
107
|
+
actions: ['secretsmanager:GetSecretValue'],
|
|
108
|
+
resources: ['*'],
|
|
109
|
+
}),
|
|
110
|
+
new PolicyStatement({
|
|
111
|
+
effect: Effect.ALLOW,
|
|
112
|
+
actions: ['kms:Decrypt'],
|
|
113
|
+
resources: ['*'],
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
public static readonly ECS_POLICY_STATEMENTS: PolicyStatement[] = [
|
|
118
|
+
EpsilonStackUtil.ALLOW_ECS,
|
|
119
|
+
EpsilonStackUtil.ALLOW_ECR,
|
|
120
|
+
EpsilonStackUtil.ALLOW_RESTRICTED_LOGS,
|
|
121
|
+
].concat(EpsilonStackUtil.ALLOW_FARGATE_SECRET_READING);
|
|
122
|
+
|
|
123
|
+
public static extractApexDomain(domainName: string): string {
|
|
124
|
+
const pieces: string[] = StringRatchet.trimToEmpty(domainName).split('.');
|
|
125
|
+
if (pieces.length < 2) {
|
|
126
|
+
ErrorRatchet.throwFormattedErr('Not a valid domain name : %s', domainName);
|
|
127
|
+
}
|
|
128
|
+
return pieces[pieces.length - 2] + '.' + pieces[pieces.length - 1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public static createForwardCorsPolicy(app: Construct, id: string, xssReportUri: string): ResponseHeadersPolicy {
|
|
132
|
+
// Creating a custom response headers policy -- all parameters optional
|
|
133
|
+
const rval: ResponseHeadersPolicy = new ResponseHeadersPolicy(app, id + 'CFRespHeadersPolicy', {
|
|
134
|
+
responseHeadersPolicyName: id + 'CustomCloudfrontPolicy',
|
|
135
|
+
comment: 'Policy allowing passthru for CORS headers',
|
|
136
|
+
corsBehavior: {
|
|
137
|
+
accessControlAllowCredentials: false,
|
|
138
|
+
accessControlAllowHeaders: ['*'],
|
|
139
|
+
accessControlAllowMethods: ['*'],
|
|
140
|
+
accessControlAllowOrigins: ['*'],
|
|
141
|
+
accessControlExposeHeaders: [],
|
|
142
|
+
accessControlMaxAge: Duration.seconds(600),
|
|
143
|
+
originOverride: false, // Use the origin values, if any
|
|
144
|
+
},
|
|
145
|
+
customHeadersBehavior: {
|
|
146
|
+
customHeaders: [
|
|
147
|
+
//{ header: 'X-Amz-Date', value: 'some-value', override: true },
|
|
148
|
+
//{ header: 'X-Amz-Security-Token', value: 'some-value', override: false },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
securityHeadersBehavior: {
|
|
152
|
+
contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true },
|
|
153
|
+
contentTypeOptions: { override: true },
|
|
154
|
+
frameOptions: { frameOption: HeadersFrameOption.DENY, override: true },
|
|
155
|
+
referrerPolicy: { referrerPolicy: HeadersReferrerPolicy.NO_REFERRER, override: true },
|
|
156
|
+
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
|
|
157
|
+
xssProtection: { protection: true, modeBlock: false, reportUri: xssReportUri, override: true },
|
|
158
|
+
},
|
|
159
|
+
removeHeaders: ['Server'],
|
|
160
|
+
serverTimingSamplingRate: 50,
|
|
161
|
+
});
|
|
162
|
+
return rval;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StackProps } from 'aws-cdk-lib';
|
|
2
|
+
import { SimpleAdditionalS3WebsiteMapping } from './simple-additional-s3-website-mapping.js';
|
|
3
|
+
import { EpsilonLambdaToCloudfrontPathMapping } from './epsilon-lambda-to-cloudfront-path-mapping.js';
|
|
4
|
+
import { Behavior } from 'aws-cdk-lib/aws-cloudfront';
|
|
5
|
+
import { EpsilonWebsiteCacheBehavior } from './epsilon-website-cache-behavior.js';
|
|
6
|
+
import { EpsilonRoute53Handling } from './epsilon-route-53-handling.js';
|
|
7
|
+
|
|
8
|
+
export interface EpsilonWebsiteStackProps extends StackProps {
|
|
9
|
+
targetBucketName: string;
|
|
10
|
+
cloudFrontHttpsCertificateArn: string;
|
|
11
|
+
cloudFrontDomainNames: string[];
|
|
12
|
+
apiMappings: EpsilonLambdaToCloudfrontPathMapping[];
|
|
13
|
+
pathsToAssets: string[];
|
|
14
|
+
route53Handling: EpsilonRoute53Handling;
|
|
15
|
+
simpleAdditionalMappings?: SimpleAdditionalS3WebsiteMapping[];
|
|
16
|
+
websiteCacheBehavior?: EpsilonWebsiteCacheBehavior;
|
|
17
|
+
websiteBehaviorOverride?: Behavior[];
|
|
18
|
+
retainWebsiteBucketOnDestroy?: boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3';
|
|
2
|
+
import { CfnOutput, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
AllowedMethods,
|
|
7
|
+
BehaviorOptions,
|
|
8
|
+
CachePolicy,
|
|
9
|
+
Distribution,
|
|
10
|
+
DistributionProps,
|
|
11
|
+
PriceClass,
|
|
12
|
+
ResponseHeadersPolicy,
|
|
13
|
+
SSLMethod,
|
|
14
|
+
ViewerProtocolPolicy,
|
|
15
|
+
} from 'aws-cdk-lib/aws-cloudfront';
|
|
16
|
+
import { HostedZone, RecordSet, RecordType } from 'aws-cdk-lib/aws-route53';
|
|
17
|
+
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
|
|
18
|
+
import { BucketDeployment, ISource, Source } from 'aws-cdk-lib/aws-s3-deployment';
|
|
19
|
+
import { EpsilonWebsiteStackProps } from './epsilon-website-stack-props.js';
|
|
20
|
+
import { EpsilonStackUtil } from './epsilon-stack-util.js';
|
|
21
|
+
import { EpsilonRoute53Handling } from './epsilon-route-53-handling';
|
|
22
|
+
import { FunctionUrlOrigin, S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
23
|
+
import { Certificate, ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
24
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
25
|
+
|
|
26
|
+
export class EpsilonWebsiteStack extends Stack {
|
|
27
|
+
constructor(scope: Construct, id: string, props?: EpsilonWebsiteStackProps) {
|
|
28
|
+
super(scope, id, props);
|
|
29
|
+
|
|
30
|
+
// Create the bucket to hold the SPA
|
|
31
|
+
const websiteBucket: Bucket = new Bucket(this, id + 'DeployBucket', {
|
|
32
|
+
bucketName: props.targetBucketName,
|
|
33
|
+
removalPolicy: props.retainWebsiteBucketOnDestroy ? undefined : RemovalPolicy.DESTROY,
|
|
34
|
+
autoDeleteObjects: !props.retainWebsiteBucketOnDestroy,
|
|
35
|
+
versioned: false,
|
|
36
|
+
publicReadAccess: false,
|
|
37
|
+
encryption: BucketEncryption.S3_MANAGED,
|
|
38
|
+
});
|
|
39
|
+
// Create the access id for that bucket
|
|
40
|
+
//const originAccessId: OriginAccessIdentity = new OriginAccessIdentity(this, id + 'OriginAccessId');
|
|
41
|
+
//websiteBucket.grantRead(originAccessId);
|
|
42
|
+
// Cache policy for that bucket
|
|
43
|
+
const cachePolicy: CachePolicy = new CachePolicy(this, id + 'ShortCachePolicy', {
|
|
44
|
+
defaultTtl: Duration.seconds(1),
|
|
45
|
+
maxTtl: Duration.seconds(1),
|
|
46
|
+
minTtl: Duration.seconds(1),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const defaultBehavior: BehaviorOptions = {
|
|
50
|
+
origin: S3BucketOrigin.withOriginAccessControl(websiteBucket), // new FunctionUrlOrigin(props.lambdaFunctionDomain),
|
|
51
|
+
compress: true,
|
|
52
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
53
|
+
cachePolicy: cachePolicy,
|
|
54
|
+
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const httpsCertificate: ICertificate = Certificate.fromCertificateArn(this, id + 'HttpsCert', props.cloudFrontHttpsCertificateArn);
|
|
58
|
+
|
|
59
|
+
const distributionProps: DistributionProps = {
|
|
60
|
+
defaultBehavior: defaultBehavior,
|
|
61
|
+
defaultRootObject: 'index.html',
|
|
62
|
+
priceClass: PriceClass.PRICE_CLASS_ALL,
|
|
63
|
+
certificate: httpsCertificate,
|
|
64
|
+
domainNames: props.cloudFrontDomainNames,
|
|
65
|
+
sslSupportMethod: SSLMethod.SNI,
|
|
66
|
+
additionalBehaviors: {}, // Will be added after
|
|
67
|
+
errorResponses: [
|
|
68
|
+
{
|
|
69
|
+
httpStatus: 404,
|
|
70
|
+
ttl: Duration.seconds(300),
|
|
71
|
+
responseHttpStatus: 200,
|
|
72
|
+
responsePagePath: '/index.html',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
httpStatus: 403,
|
|
76
|
+
ttl: Duration.seconds(300),
|
|
77
|
+
responseHttpStatus: 200,
|
|
78
|
+
responsePagePath: '/index.html',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add api sources, if any
|
|
84
|
+
(props.apiMappings || []).forEach((s) => {
|
|
85
|
+
const next: BehaviorOptions = {
|
|
86
|
+
origin: new FunctionUrlOrigin(s.lambdaFunctionUrl),
|
|
87
|
+
compress: true,
|
|
88
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
89
|
+
cachePolicy: CachePolicy.CACHING_DISABLED,
|
|
90
|
+
allowedMethods: AllowedMethods.ALLOW_ALL,
|
|
91
|
+
responseHeadersPolicy: s.responseHeadersPolicyCreator
|
|
92
|
+
? s.responseHeadersPolicyCreator(this, id)
|
|
93
|
+
: ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS,
|
|
94
|
+
};
|
|
95
|
+
distributionProps.additionalBehaviors[s.pathPattern] = next;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Add extra bucket mappings, if any
|
|
99
|
+
// They are assumed to have been created outside of Epsilon, so they are imported not created
|
|
100
|
+
(props.simpleAdditionalMappings || []).forEach((eb) => {
|
|
101
|
+
const nextBucket = Bucket.fromBucketAttributes(this, eb.bucketName + 'ImportedBucket', {
|
|
102
|
+
bucketName: eb.bucketName,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const behaviorOptions: BehaviorOptions = {
|
|
106
|
+
origin: S3BucketOrigin.withOriginAccessControl(nextBucket),
|
|
107
|
+
compress: true,
|
|
108
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
109
|
+
cachePolicy: cachePolicy,
|
|
110
|
+
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
distributionProps.additionalBehaviors[eb.pathPattern] = behaviorOptions;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create the distro
|
|
117
|
+
const cloudfrontDistro: Distribution = new Distribution(this, id + 'CloudfrontDistro', distributionProps);
|
|
118
|
+
new CfnOutput(this, 'DistributionUrl', { value: 'https://' + cloudfrontDistro.domainName });
|
|
119
|
+
|
|
120
|
+
// Have to be able to skip this since SOME people don't do DNS in Route53
|
|
121
|
+
if (props?.route53Handling === EpsilonRoute53Handling.Update) {
|
|
122
|
+
if (props?.cloudFrontDomainNames?.length) {
|
|
123
|
+
props.cloudFrontDomainNames.forEach((dn, _idx) => {
|
|
124
|
+
const _domain: RecordSet = new RecordSet(this, id + 'DomainName-' + dn, {
|
|
125
|
+
recordType: RecordType.A,
|
|
126
|
+
recordName: dn,
|
|
127
|
+
target: {
|
|
128
|
+
aliasTarget: new CloudFrontTarget(cloudfrontDistro),
|
|
129
|
+
},
|
|
130
|
+
zone: HostedZone.fromLookup(this, id, { domainName: EpsilonStackUtil.extractApexDomain(dn) }),
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const assetSources: ISource[] = props.pathsToAssets.map((inPath) => Source.asset(path.resolve(inPath)));
|
|
137
|
+
Logger.info('Found %d asset sources to push to S3', assetSources.length);
|
|
138
|
+
|
|
139
|
+
// Sync files to the S3 Bucket
|
|
140
|
+
// [Source.asset(path.resolve('../website/dist'))],
|
|
141
|
+
new BucketDeployment(this, id + 'SiteDeploy', {
|
|
142
|
+
sources: assetSources,
|
|
143
|
+
destinationBucket: websiteBucket,
|
|
144
|
+
distribution: cloudfrontDistro,
|
|
145
|
+
distributionPaths: ['/*'], //'/locales/*', '/index.html', '/manifest.webmanifest', '/service-worker.js']
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by barrelsby.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './cdk/bucket-and-behavior-options';
|
|
6
|
+
export * from './cdk/epsilon-api-stack-feature.js';
|
|
7
|
+
export * from './cdk/epsilon-api-stack-props.js';
|
|
8
|
+
export * from './cdk/epsilon-api-stack.js';
|
|
9
|
+
export * from './cdk/epsilon-lambda-to-cloudfront-path-mapping.js';
|
|
10
|
+
export * from './cdk/epsilon-stack-util.js';
|
|
11
|
+
export * from './cdk/epsilon-website-cache-behavior.js';
|
|
12
|
+
export * from './cdk/epsilon-website-stack-props.js';
|
|
13
|
+
export * from './cdk/epsilon-website-stack.js';
|
|
14
|
+
export * from './cdk/simple-additional-s3-website-mapping.js';
|