@maestro-js/aws-cdk-recipes 1.0.0-alpha.0
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/README.md +391 -0
- package/dist/index.d.ts +2284 -0
- package/dist/index.js +2252 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2252 @@
|
|
|
1
|
+
// src/bucket.ts
|
|
2
|
+
import { Construct } from "constructs";
|
|
3
|
+
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
4
|
+
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
5
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
6
|
+
var Bucket2 = class extends Construct {
|
|
7
|
+
bucket;
|
|
8
|
+
constructor(scope, id, props = {}) {
|
|
9
|
+
super(scope, id);
|
|
10
|
+
const cors = resolveCorsRules(props.cors);
|
|
11
|
+
let bucketProps = {
|
|
12
|
+
blockPublicAccess: props.access === "public" ? resolvePublicAccessBlock() : s3.BlockPublicAccess.BLOCK_ALL,
|
|
13
|
+
objectOwnership: props.access === "public" ? s3.ObjectOwnership.BUCKET_OWNER_ENFORCED : void 0,
|
|
14
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
15
|
+
versioned: props.versioning ?? false,
|
|
16
|
+
cors,
|
|
17
|
+
lifecycleRules: props.lifecycle ? resolveLifecycleRules(props.lifecycle) : void 0
|
|
18
|
+
};
|
|
19
|
+
if (props.transform?.bucket) {
|
|
20
|
+
bucketProps = props.transform.bucket(bucketProps);
|
|
21
|
+
}
|
|
22
|
+
this.bucket = new s3.Bucket(this, "Bucket", bucketProps);
|
|
23
|
+
if (props.enforceHttps !== false) {
|
|
24
|
+
this.bucket.addToResourcePolicy(
|
|
25
|
+
new iam.PolicyStatement({
|
|
26
|
+
effect: iam.Effect.DENY,
|
|
27
|
+
principals: [new iam.AnyPrincipal()],
|
|
28
|
+
actions: ["s3:*"],
|
|
29
|
+
resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
|
|
30
|
+
conditions: { Bool: { "aws:SecureTransport": "false" } }
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (props.access === "public") {
|
|
35
|
+
this.bucket.addToResourcePolicy(
|
|
36
|
+
new iam.PolicyStatement({
|
|
37
|
+
effect: iam.Effect.ALLOW,
|
|
38
|
+
principals: [new iam.AnyPrincipal()],
|
|
39
|
+
actions: ["s3:GetObject"],
|
|
40
|
+
resources: [`${this.bucket.bucketArn}/*`]
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (props.policy) {
|
|
45
|
+
for (const stmt of props.policy) {
|
|
46
|
+
this.bucket.addToResourcePolicy(resolvePolicyStatement(this.bucket, stmt));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function resolvePublicAccessBlock() {
|
|
52
|
+
return new s3.BlockPublicAccess({
|
|
53
|
+
blockPublicAcls: true,
|
|
54
|
+
ignorePublicAcls: true,
|
|
55
|
+
blockPublicPolicy: false,
|
|
56
|
+
restrictPublicBuckets: false
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function resolveCorsRules(cors) {
|
|
60
|
+
if (cors === false) return void 0;
|
|
61
|
+
if (cors === void 0) {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
allowedHeaders: ["*"],
|
|
65
|
+
allowedMethods: [
|
|
66
|
+
s3.HttpMethods.GET,
|
|
67
|
+
s3.HttpMethods.PUT,
|
|
68
|
+
s3.HttpMethods.POST,
|
|
69
|
+
s3.HttpMethods.DELETE,
|
|
70
|
+
s3.HttpMethods.HEAD
|
|
71
|
+
],
|
|
72
|
+
allowedOrigins: ["*"]
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
allowedHeaders: cors.allowHeaders,
|
|
79
|
+
allowedMethods: (cors.allowMethods ?? ["GET"]).map(resolveCorsMethods),
|
|
80
|
+
allowedOrigins: cors.allowOrigins ?? ["*"],
|
|
81
|
+
exposedHeaders: cors.exposeHeaders,
|
|
82
|
+
maxAge: cors.maxAge
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function resolveCorsMethods(method) {
|
|
87
|
+
switch (method) {
|
|
88
|
+
case "GET":
|
|
89
|
+
return s3.HttpMethods.GET;
|
|
90
|
+
case "PUT":
|
|
91
|
+
return s3.HttpMethods.PUT;
|
|
92
|
+
case "POST":
|
|
93
|
+
return s3.HttpMethods.POST;
|
|
94
|
+
case "DELETE":
|
|
95
|
+
return s3.HttpMethods.DELETE;
|
|
96
|
+
case "HEAD":
|
|
97
|
+
return s3.HttpMethods.HEAD;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function resolveLifecycleRules(rules) {
|
|
101
|
+
return rules.map((rule) => {
|
|
102
|
+
if (!rule.expiresIn && !rule.expiresAt) {
|
|
103
|
+
throw new Error('LifecycleRule requires either "expiresIn" or "expiresAt"');
|
|
104
|
+
}
|
|
105
|
+
const expirationDate = rule.expiresAt ? new Date(rule.expiresAt) : void 0;
|
|
106
|
+
if (expirationDate && isNaN(expirationDate.getTime())) {
|
|
107
|
+
throw new Error(`Invalid "expiresAt" date: "${rule.expiresAt}"`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
id: rule.id,
|
|
111
|
+
prefix: rule.prefix,
|
|
112
|
+
enabled: rule.enabled ?? true,
|
|
113
|
+
expiration: rule.expiresIn ? parseDurationDays(rule.expiresIn) : void 0,
|
|
114
|
+
expirationDate
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function parseDurationDays(duration) {
|
|
119
|
+
const match = duration.match(/^(\d+)\s*days?$/);
|
|
120
|
+
if (!match) throw new Error(`Invalid duration: "${duration}". Expected format: "30 days"`);
|
|
121
|
+
return Duration.days(Number(match[1]));
|
|
122
|
+
}
|
|
123
|
+
function resolvePolicyStatement(bucket, stmt) {
|
|
124
|
+
const principals = resolvePrincipals(stmt.principals);
|
|
125
|
+
const resources = stmt.paths ? stmt.paths.map((r) => `${bucket.bucketArn}/${r}`) : [bucket.bucketArn, `${bucket.bucketArn}/*`];
|
|
126
|
+
return new iam.PolicyStatement({
|
|
127
|
+
effect: stmt.effect === "deny" ? iam.Effect.DENY : iam.Effect.ALLOW,
|
|
128
|
+
principals,
|
|
129
|
+
actions: stmt.actions,
|
|
130
|
+
resources,
|
|
131
|
+
conditions: stmt.conditions ? resolveConditions(stmt.conditions) : void 0
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function resolvePrincipals(principals) {
|
|
135
|
+
if (principals === "*") return [new iam.AnyPrincipal()];
|
|
136
|
+
return principals.flatMap((p) => {
|
|
137
|
+
switch (p.type) {
|
|
138
|
+
case "aws":
|
|
139
|
+
return p.identifiers.map((id) => new iam.ArnPrincipal(id));
|
|
140
|
+
case "service":
|
|
141
|
+
return p.identifiers.map((id) => new iam.ServicePrincipal(id));
|
|
142
|
+
case "federated":
|
|
143
|
+
return p.identifiers.map((id) => new iam.FederatedPrincipal(id, {}));
|
|
144
|
+
case "canonical":
|
|
145
|
+
return p.identifiers.map((id) => new iam.CanonicalUserPrincipal(id));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function resolveConditions(conditions) {
|
|
150
|
+
const result = {};
|
|
151
|
+
for (const c of conditions) {
|
|
152
|
+
const test = result[c.test] ??= {};
|
|
153
|
+
const existing = test[c.variable];
|
|
154
|
+
test[c.variable] = existing ? [...existing, ...c.values] : c.values;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/vpc.ts
|
|
160
|
+
import { Construct as Construct2 } from "constructs";
|
|
161
|
+
import { Stack } from "aws-cdk-lib";
|
|
162
|
+
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
|
163
|
+
import * as iam2 from "aws-cdk-lib/aws-iam";
|
|
164
|
+
import * as logs from "aws-cdk-lib/aws-logs";
|
|
165
|
+
var Vpc2 = class extends Construct2 {
|
|
166
|
+
vpc;
|
|
167
|
+
publicSubnets;
|
|
168
|
+
privateSubnets;
|
|
169
|
+
securityGroup;
|
|
170
|
+
wireguard;
|
|
171
|
+
wireguardPublicKeyParam;
|
|
172
|
+
constructor(scope, id, props = {}) {
|
|
173
|
+
super(scope, id);
|
|
174
|
+
const maxAzs = props.maxAzs ?? 2;
|
|
175
|
+
const natOption = props.natGateways ?? "single";
|
|
176
|
+
const { natGateways, natGatewayProvider } = resolveNat(natOption, maxAzs);
|
|
177
|
+
const hasEgress = natOption !== "none";
|
|
178
|
+
const defaultSubnets = [
|
|
179
|
+
{ name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
|
|
180
|
+
{
|
|
181
|
+
name: "Private",
|
|
182
|
+
subnetType: hasEgress ? ec2.SubnetType.PRIVATE_WITH_EGRESS : ec2.SubnetType.PRIVATE_ISOLATED,
|
|
183
|
+
cidrMask: 24
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
this.vpc = new ec2.Vpc(this, "Vpc", {
|
|
187
|
+
ipAddresses: ec2.IpAddresses.cidr(props.cidr ?? "10.0.0.0/16"),
|
|
188
|
+
maxAzs,
|
|
189
|
+
natGateways,
|
|
190
|
+
natGatewayProvider,
|
|
191
|
+
subnetConfiguration: props.subnetConfiguration ?? defaultSubnets
|
|
192
|
+
});
|
|
193
|
+
this.publicSubnets = this.vpc.publicSubnets;
|
|
194
|
+
this.privateSubnets = hasEgress ? this.vpc.privateSubnets : this.vpc.isolatedSubnets;
|
|
195
|
+
this.securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
|
|
196
|
+
vpc: this.vpc,
|
|
197
|
+
description: `Default security group for ${id}`,
|
|
198
|
+
allowAllOutbound: true
|
|
199
|
+
});
|
|
200
|
+
if (props.wireguard) {
|
|
201
|
+
const region = Stack.of(this).region;
|
|
202
|
+
const privateKeyParam = `/wireguard/${id}/private-key`;
|
|
203
|
+
const publicKeyParam = `/wireguard/${id}/public-key`;
|
|
204
|
+
const peersParam = `/wireguard/${id}/peers`;
|
|
205
|
+
this.wireguardPublicKeyParam = publicKeyParam;
|
|
206
|
+
const wgSg = new ec2.SecurityGroup(this, "WireGuardSG", {
|
|
207
|
+
vpc: this.vpc,
|
|
208
|
+
description: "WireGuard VPN",
|
|
209
|
+
allowAllOutbound: true
|
|
210
|
+
});
|
|
211
|
+
wgSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.udp(51820), "WireGuard");
|
|
212
|
+
const vpcCidr = props.cidr ?? "10.0.0.0/16";
|
|
213
|
+
const userData = ec2.UserData.forLinux();
|
|
214
|
+
userData.addCommands(
|
|
215
|
+
// Install WireGuard
|
|
216
|
+
"dnf install -y wireguard-tools iptables-nft",
|
|
217
|
+
// Enable IP forwarding
|
|
218
|
+
"sysctl -w net.ipv4.ip_forward=1",
|
|
219
|
+
'echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf',
|
|
220
|
+
// Retrieve or generate server keys
|
|
221
|
+
"umask 077",
|
|
222
|
+
`if aws ssm get-parameter --name "${privateKeyParam}" --with-decryption --region ${region} --query "Parameter.Value" --output text > /etc/wireguard/server.key 2>/dev/null; then`,
|
|
223
|
+
" wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub",
|
|
224
|
+
"else",
|
|
225
|
+
" wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub",
|
|
226
|
+
` aws ssm put-parameter --name "${privateKeyParam}" --type SecureString --value "$(cat /etc/wireguard/server.key)" --region ${region}`,
|
|
227
|
+
` aws ssm put-parameter --name "${publicKeyParam}" --type String --value "$(cat /etc/wireguard/server.pub)" --region ${region}`,
|
|
228
|
+
"fi",
|
|
229
|
+
// Create wg0 config
|
|
230
|
+
"ENI=$(ip -4 route show default | awk '{print $5}')",
|
|
231
|
+
`cat > /etc/wireguard/wg0.conf << EOF`,
|
|
232
|
+
"[Interface]",
|
|
233
|
+
"Address = 10.100.0.1/24",
|
|
234
|
+
"ListenPort = 51820",
|
|
235
|
+
"PrivateKey = $(cat /etc/wireguard/server.key)",
|
|
236
|
+
"",
|
|
237
|
+
"PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -o $ENI -j MASQUERADE",
|
|
238
|
+
`PostUp = iptables -A FORWARD -s 10.100.0.0/24 -d ${vpcCidr} -j ACCEPT`,
|
|
239
|
+
"PostDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/24 -o $ENI -j MASQUERADE",
|
|
240
|
+
`PostDown = iptables -D FORWARD -s 10.100.0.0/24 -d ${vpcCidr} -j ACCEPT`,
|
|
241
|
+
"EOF",
|
|
242
|
+
// Load persisted peers from SSM
|
|
243
|
+
`if aws ssm get-parameter --name "${peersParam}" --with-decryption --region ${region} --query "Parameter.Value" --output text >> /etc/wireguard/wg0.conf 2>/dev/null; then`,
|
|
244
|
+
' echo "Loaded persisted peers"',
|
|
245
|
+
"fi",
|
|
246
|
+
// Enable and start WireGuard
|
|
247
|
+
"systemctl enable wg-quick@wg0",
|
|
248
|
+
"systemctl start wg-quick@wg0",
|
|
249
|
+
// Store metadata for CLI tooling
|
|
250
|
+
'TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")',
|
|
251
|
+
'PUBLIC_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4)',
|
|
252
|
+
'INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)',
|
|
253
|
+
`aws ssm put-parameter --name "/wireguard/${id}/endpoint" --type String --value "$PUBLIC_IP" --overwrite --region ${region}`,
|
|
254
|
+
`aws ssm put-parameter --name "/wireguard/${id}/instance-id" --type String --value "$INSTANCE_ID" --overwrite --region ${region}`
|
|
255
|
+
);
|
|
256
|
+
this.wireguard = new ec2.Instance(this, "WireGuard", {
|
|
257
|
+
vpc: this.vpc,
|
|
258
|
+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
|
|
259
|
+
machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64 }),
|
|
260
|
+
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
|
|
261
|
+
securityGroup: wgSg,
|
|
262
|
+
ssmSessionPermissions: true,
|
|
263
|
+
userData
|
|
264
|
+
});
|
|
265
|
+
this.wireguard.addSecurityGroup(this.securityGroup);
|
|
266
|
+
const ssmParamArn = Stack.of(this).formatArn({
|
|
267
|
+
service: "ssm",
|
|
268
|
+
resource: "parameter",
|
|
269
|
+
resourceName: `wireguard/${id}/*`
|
|
270
|
+
});
|
|
271
|
+
this.wireguard.role.addToPrincipalPolicy(
|
|
272
|
+
new iam2.PolicyStatement({
|
|
273
|
+
actions: ["ssm:GetParameter", "ssm:PutParameter"],
|
|
274
|
+
resources: [ssmParamArn]
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
const eip = new ec2.CfnEIP(this, "WireGuardEIP", {
|
|
278
|
+
instanceId: this.wireguard.instanceId
|
|
279
|
+
});
|
|
280
|
+
const gatewayAttachment = this.vpc.node.findChild("VPCGW");
|
|
281
|
+
eip.addDependency(gatewayAttachment);
|
|
282
|
+
}
|
|
283
|
+
if (props.flowLogs) {
|
|
284
|
+
const flowLogOpts = typeof props.flowLogs === "object" ? props.flowLogs : {};
|
|
285
|
+
const logGroup = new logs.LogGroup(this, "FlowLogGroup", {
|
|
286
|
+
retention: flowLogOpts.retention ?? logs.RetentionDays.ONE_MONTH
|
|
287
|
+
});
|
|
288
|
+
this.vpc.addFlowLog("FlowLog", {
|
|
289
|
+
destination: ec2.FlowLogDestination.toCloudWatchLogs(logGroup)
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function resolveNat(option, maxAzs) {
|
|
295
|
+
switch (option) {
|
|
296
|
+
case "none":
|
|
297
|
+
return { natGateways: 0 };
|
|
298
|
+
case "ec2":
|
|
299
|
+
return {
|
|
300
|
+
natGateways: 1,
|
|
301
|
+
natGatewayProvider: ec2.NatProvider.instanceV2({
|
|
302
|
+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
|
|
303
|
+
machineImage: new ec2.LookupMachineImage({
|
|
304
|
+
name: "fck-nat-al2023-*-arm64-ebs",
|
|
305
|
+
owners: ["568608671756"]
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
};
|
|
309
|
+
case "onePerAz":
|
|
310
|
+
return { natGateways: maxAzs };
|
|
311
|
+
case "single":
|
|
312
|
+
default:
|
|
313
|
+
return { natGateways: 1 };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/cluster.ts
|
|
318
|
+
import { Construct as Construct3 } from "constructs";
|
|
319
|
+
import * as ecs from "aws-cdk-lib/aws-ecs";
|
|
320
|
+
var Cluster2 = class extends Construct3 {
|
|
321
|
+
cluster;
|
|
322
|
+
constructor(scope, id, props) {
|
|
323
|
+
super(scope, id);
|
|
324
|
+
let clusterProps = {
|
|
325
|
+
vpc: props.vpc,
|
|
326
|
+
enableFargateCapacityProviders: props.enableFargateCapacityProviders ?? false
|
|
327
|
+
};
|
|
328
|
+
if (props.transform?.cluster) {
|
|
329
|
+
clusterProps = props.transform.cluster(clusterProps);
|
|
330
|
+
}
|
|
331
|
+
this.cluster = new ecs.Cluster(this, "Cluster", clusterProps);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/static-site.ts
|
|
336
|
+
import { Construct as Construct6 } from "constructs";
|
|
337
|
+
import { RemovalPolicy as RemovalPolicy2 } from "aws-cdk-lib";
|
|
338
|
+
import * as s33 from "aws-cdk-lib/aws-s3";
|
|
339
|
+
import * as cloudfront2 from "aws-cdk-lib/aws-cloudfront";
|
|
340
|
+
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
341
|
+
|
|
342
|
+
// src/helpers/cloudfront.ts
|
|
343
|
+
import "constructs";
|
|
344
|
+
import "aws-cdk-lib/aws-s3";
|
|
345
|
+
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
|
|
346
|
+
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
|
|
347
|
+
import * as acm from "aws-cdk-lib/aws-certificatemanager";
|
|
348
|
+
function resolveCachePolicy(policy) {
|
|
349
|
+
if (!policy) return cloudfront.CachePolicy.CACHING_OPTIMIZED;
|
|
350
|
+
if (typeof policy === "string") {
|
|
351
|
+
switch (policy) {
|
|
352
|
+
case "CACHING_OPTIMIZED":
|
|
353
|
+
return cloudfront.CachePolicy.CACHING_OPTIMIZED;
|
|
354
|
+
case "CACHING_DISABLED":
|
|
355
|
+
return cloudfront.CachePolicy.CACHING_DISABLED;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return policy;
|
|
359
|
+
}
|
|
360
|
+
function resolveInvalidation(invalidation) {
|
|
361
|
+
if (invalidation === false) {
|
|
362
|
+
return { enabled: false, paths: [], wait: false };
|
|
363
|
+
}
|
|
364
|
+
if (invalidation === void 0) {
|
|
365
|
+
return { enabled: true, paths: ["/*"], wait: true };
|
|
366
|
+
}
|
|
367
|
+
const paths = invalidation.paths === "all" || invalidation.paths === void 0 ? ["/*"] : invalidation.paths;
|
|
368
|
+
return { enabled: true, paths, wait: invalidation.wait ?? true };
|
|
369
|
+
}
|
|
370
|
+
function resolveAssetDeployment(scope, id, opts) {
|
|
371
|
+
const invalidation = resolveInvalidation(opts.invalidation);
|
|
372
|
+
const baseDeployment = new s3deploy.BucketDeployment(scope, `${id}AssetDeployment`, {
|
|
373
|
+
sources: [s3deploy.Source.asset(opts.path)],
|
|
374
|
+
destinationBucket: opts.bucket,
|
|
375
|
+
destinationKeyPrefix: opts.prefix,
|
|
376
|
+
prune: true
|
|
377
|
+
});
|
|
378
|
+
opts.fileOptions.forEach((option, i) => {
|
|
379
|
+
const files = Array.isArray(option.files) ? option.files : [option.files];
|
|
380
|
+
const ignore = option.ignore ? Array.isArray(option.ignore) ? option.ignore : [option.ignore] : void 0;
|
|
381
|
+
const isLast = i === opts.fileOptions.length - 1;
|
|
382
|
+
const cacheControl = option.cacheControl ? s3deploy.CacheControl.fromString(option.cacheControl) : void 0;
|
|
383
|
+
const overlay = new s3deploy.BucketDeployment(scope, `${id}AssetDeploymentOverlay${i}`, {
|
|
384
|
+
sources: [s3deploy.Source.asset(opts.path)],
|
|
385
|
+
destinationBucket: opts.bucket,
|
|
386
|
+
destinationKeyPrefix: opts.prefix,
|
|
387
|
+
exclude: ["*", ...ignore ?? []],
|
|
388
|
+
include: files,
|
|
389
|
+
prune: false,
|
|
390
|
+
cacheControl: cacheControl ? [cacheControl] : void 0,
|
|
391
|
+
contentType: option.contentType,
|
|
392
|
+
distribution: isLast && invalidation.enabled ? opts.distribution : void 0,
|
|
393
|
+
distributionPaths: isLast && invalidation.enabled ? invalidation.paths : void 0
|
|
394
|
+
});
|
|
395
|
+
overlay.node.addDependency(baseDeployment);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function resolveCertificate(scope, domain) {
|
|
399
|
+
const certificate = domain ? acm.Certificate.fromCertificateArn(scope, "Certificate", domain.certificateArn) : void 0;
|
|
400
|
+
const domainNames = domain ? [domain.name, ...domain.aliases ?? []] : void 0;
|
|
401
|
+
return { certificate, domainNames };
|
|
402
|
+
}
|
|
403
|
+
function resolveAdditionalBehaviors(s3Origin, behaviors) {
|
|
404
|
+
const additionalBehaviors = {};
|
|
405
|
+
for (const behavior of behaviors) {
|
|
406
|
+
additionalBehaviors[behavior.pathPattern] = {
|
|
407
|
+
origin: s3Origin,
|
|
408
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
409
|
+
cachePolicy: resolveCachePolicy(behavior.cachePolicy)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return additionalBehaviors;
|
|
413
|
+
}
|
|
414
|
+
function createViewerRequestFunction(scope, id) {
|
|
415
|
+
return new cloudfront.Function(scope, id, {
|
|
416
|
+
code: cloudfront.FunctionCode.fromInline(`
|
|
417
|
+
function handler(event) {
|
|
418
|
+
var request = event.request;
|
|
419
|
+
var uri = request.uri;
|
|
420
|
+
var host = request.headers.host ? request.headers.host.value : '';
|
|
421
|
+
request.headers['x-forwarded-host'] = { value: host };
|
|
422
|
+
if (uri.length > 1 && uri.endsWith('/')) {
|
|
423
|
+
request.uri = uri.slice(0, -1);
|
|
424
|
+
}
|
|
425
|
+
return request;
|
|
426
|
+
}`)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/helpers/ecs.ts
|
|
431
|
+
import "constructs";
|
|
432
|
+
import * as logs2 from "aws-cdk-lib/aws-logs";
|
|
433
|
+
import * as ecs2 from "aws-cdk-lib/aws-ecs";
|
|
434
|
+
import * as ssm from "aws-cdk-lib/aws-ssm";
|
|
435
|
+
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
|
436
|
+
function resolveLogRetention(retention) {
|
|
437
|
+
switch (retention) {
|
|
438
|
+
case "1 day":
|
|
439
|
+
return logs2.RetentionDays.ONE_DAY;
|
|
440
|
+
case "3 days":
|
|
441
|
+
return logs2.RetentionDays.THREE_DAYS;
|
|
442
|
+
case "5 days":
|
|
443
|
+
return logs2.RetentionDays.FIVE_DAYS;
|
|
444
|
+
case "1 week":
|
|
445
|
+
return logs2.RetentionDays.ONE_WEEK;
|
|
446
|
+
case "2 weeks":
|
|
447
|
+
return logs2.RetentionDays.TWO_WEEKS;
|
|
448
|
+
case "1 month":
|
|
449
|
+
case void 0:
|
|
450
|
+
return logs2.RetentionDays.ONE_MONTH;
|
|
451
|
+
case "3 months":
|
|
452
|
+
return logs2.RetentionDays.THREE_MONTHS;
|
|
453
|
+
case "6 months":
|
|
454
|
+
return logs2.RetentionDays.SIX_MONTHS;
|
|
455
|
+
case "1 year":
|
|
456
|
+
return logs2.RetentionDays.ONE_YEAR;
|
|
457
|
+
case "2 years":
|
|
458
|
+
return logs2.RetentionDays.TWO_YEARS;
|
|
459
|
+
case "forever":
|
|
460
|
+
return logs2.RetentionDays.INFINITE;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function resolveSecrets(scope, ssmEntries, explicitSecrets, prefix) {
|
|
464
|
+
const merged = {};
|
|
465
|
+
let hasEntries = false;
|
|
466
|
+
const p = prefix ? `${prefix}-` : "";
|
|
467
|
+
if (ssmEntries) {
|
|
468
|
+
let i = 0;
|
|
469
|
+
for (const [envVar, arn] of Object.entries(ssmEntries)) {
|
|
470
|
+
hasEntries = true;
|
|
471
|
+
if (arn.startsWith("arn:aws:secretsmanager:")) {
|
|
472
|
+
merged[envVar] = ecs2.Secret.fromSecretsManager(
|
|
473
|
+
secretsmanager.Secret.fromSecretCompleteArn(scope, `${p}Secret${i}`, arn)
|
|
474
|
+
);
|
|
475
|
+
} else {
|
|
476
|
+
merged[envVar] = ecs2.Secret.fromSsmParameter(
|
|
477
|
+
ssm.StringParameter.fromSecureStringParameterAttributes(scope, `${p}SsmParam${i}`, { parameterName: arn })
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
i++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (explicitSecrets) {
|
|
484
|
+
hasEntries = true;
|
|
485
|
+
Object.assign(merged, explicitSecrets);
|
|
486
|
+
}
|
|
487
|
+
return hasEntries ? merged : void 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/helpers/docker.ts
|
|
491
|
+
import { execSync } from "child_process";
|
|
492
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
493
|
+
import { dirname, relative, resolve } from "path";
|
|
494
|
+
function findMonorepoRoot(startDir) {
|
|
495
|
+
let dir = startDir;
|
|
496
|
+
while (true) {
|
|
497
|
+
if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) return dir;
|
|
498
|
+
const parent = dirname(dir);
|
|
499
|
+
if (parent === dir) return null;
|
|
500
|
+
dir = parent;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function readPackageName(appRoot) {
|
|
504
|
+
const raw = readFileSync(resolve(appRoot, "package.json"), "utf-8");
|
|
505
|
+
const pkg = JSON.parse(raw);
|
|
506
|
+
if (!pkg.name) {
|
|
507
|
+
throw new Error(`package.json at "${appRoot}" is missing a "name" field`);
|
|
508
|
+
}
|
|
509
|
+
return pkg.name;
|
|
510
|
+
}
|
|
511
|
+
function buildMonorepoDockerfile(buildDir, port) {
|
|
512
|
+
return `FROM node:22-slim
|
|
513
|
+
WORKDIR /app
|
|
514
|
+
ENV NODE_ENV=production
|
|
515
|
+
COPY package.json .
|
|
516
|
+
COPY node_modules/ ./node_modules/
|
|
517
|
+
COPY ${buildDir}/server/ ./${buildDir}/server/
|
|
518
|
+
EXPOSE ${port}
|
|
519
|
+
CMD ["./node_modules/.bin/react-router-serve", "./${buildDir}/server/index.js"]
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
function resolveReactRouterDockerContext(opts) {
|
|
523
|
+
const { appRoot, buildDir, basePath, port, className } = opts;
|
|
524
|
+
const serverBuild = `${basePath}/server/index.js`;
|
|
525
|
+
if (!existsSync(serverBuild)) {
|
|
526
|
+
throw new Error(`${className}: "${serverBuild}" not found. Run "react-router build" before deploying.`);
|
|
527
|
+
}
|
|
528
|
+
if (existsSync(`${appRoot}/Dockerfile`)) {
|
|
529
|
+
return { context: appRoot };
|
|
530
|
+
}
|
|
531
|
+
const absAppRoot = resolve(appRoot);
|
|
532
|
+
const monorepoRoot = findMonorepoRoot(absAppRoot);
|
|
533
|
+
if (monorepoRoot) {
|
|
534
|
+
const packageName = readPackageName(absAppRoot);
|
|
535
|
+
const deployDir = resolve(absAppRoot, buildDir, "_deploy");
|
|
536
|
+
rmSync(deployDir, { recursive: true, force: true });
|
|
537
|
+
execSync(`pnpm --filter "${packageName}" deploy --prod ${deployDir}`, {
|
|
538
|
+
cwd: monorepoRoot,
|
|
539
|
+
stdio: "inherit"
|
|
540
|
+
});
|
|
541
|
+
const destBuildDir = resolve(deployDir, buildDir, "server");
|
|
542
|
+
mkdirSync(destBuildDir, { recursive: true });
|
|
543
|
+
cpSync(resolve(absAppRoot, buildDir, "server"), destBuildDir, { recursive: true });
|
|
544
|
+
writeFileSync(resolve(deployDir, "Dockerfile"), buildMonorepoDockerfile(buildDir, port));
|
|
545
|
+
return { context: relative(".", deployDir) || "." };
|
|
546
|
+
}
|
|
547
|
+
writeFileSync(`${basePath}/_Dockerfile`, buildStandaloneDockerfile(buildDir, port));
|
|
548
|
+
return { context: appRoot, dockerfile: `${buildDir}/_Dockerfile` };
|
|
549
|
+
}
|
|
550
|
+
function buildStandaloneDockerfile(buildDir, port) {
|
|
551
|
+
return `# syntax=docker/dockerfile:1.5
|
|
552
|
+
|
|
553
|
+
FROM node:22-slim AS base
|
|
554
|
+
WORKDIR /app
|
|
555
|
+
ENV CI=true
|
|
556
|
+
RUN corepack enable
|
|
557
|
+
|
|
558
|
+
COPY package.json pnpm-lock.yaml ./
|
|
559
|
+
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod
|
|
560
|
+
|
|
561
|
+
COPY ${buildDir}/ ./${buildDir}/
|
|
562
|
+
|
|
563
|
+
ENV NODE_ENV=production
|
|
564
|
+
EXPOSE ${port}
|
|
565
|
+
CMD ["./node_modules/.bin/react-router-serve", "./${buildDir}/server/index.js"]
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/static-site.ts
|
|
570
|
+
var defaultFileOptions = [
|
|
571
|
+
{ files: "**", cacheControl: "max-age=31536000,public,immutable" },
|
|
572
|
+
{ files: "**/*.html", cacheControl: "max-age=0,no-cache,no-store,must-revalidate" }
|
|
573
|
+
];
|
|
574
|
+
var StaticSite = class extends Construct6 {
|
|
575
|
+
bucket;
|
|
576
|
+
distribution;
|
|
577
|
+
constructor(scope, id, props) {
|
|
578
|
+
super(scope, id);
|
|
579
|
+
const indexPage = props.indexPage ?? "index.html";
|
|
580
|
+
const errorPage = props.errorPage ?? indexPage;
|
|
581
|
+
const fileOptions = props.assets?.fileOptions ?? defaultFileOptions;
|
|
582
|
+
let bucketProps = {
|
|
583
|
+
blockPublicAccess: s33.BlockPublicAccess.BLOCK_ALL,
|
|
584
|
+
removalPolicy: RemovalPolicy2.RETAIN
|
|
585
|
+
};
|
|
586
|
+
if (props.transform?.bucket) {
|
|
587
|
+
bucketProps = props.transform.bucket(bucketProps);
|
|
588
|
+
}
|
|
589
|
+
this.bucket = new s33.Bucket(this, "Bucket", bucketProps);
|
|
590
|
+
const { certificate, domainNames } = resolveCertificate(this, props.domain);
|
|
591
|
+
const functionAssociations = [];
|
|
592
|
+
if (props.edge?.viewerRequest) {
|
|
593
|
+
functionAssociations.push({
|
|
594
|
+
function: props.edge.viewerRequest,
|
|
595
|
+
eventType: cloudfront2.FunctionEventType.VIEWER_REQUEST
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (props.edge?.viewerResponse) {
|
|
599
|
+
functionAssociations.push({
|
|
600
|
+
function: props.edge.viewerResponse,
|
|
601
|
+
eventType: cloudfront2.FunctionEventType.VIEWER_RESPONSE
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
let distributionProps = {
|
|
605
|
+
defaultBehavior: {
|
|
606
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
|
|
607
|
+
viewerProtocolPolicy: cloudfront2.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
608
|
+
functionAssociations: functionAssociations.length > 0 ? functionAssociations : void 0
|
|
609
|
+
},
|
|
610
|
+
defaultRootObject: indexPage,
|
|
611
|
+
domainNames,
|
|
612
|
+
certificate,
|
|
613
|
+
errorResponses: [
|
|
614
|
+
{ httpStatus: 403, responseHttpStatus: 200, responsePagePath: `/${errorPage}` },
|
|
615
|
+
{ httpStatus: 404, responseHttpStatus: 200, responsePagePath: `/${errorPage}` }
|
|
616
|
+
]
|
|
617
|
+
};
|
|
618
|
+
if (props.transform?.distribution) {
|
|
619
|
+
distributionProps = props.transform.distribution(distributionProps);
|
|
620
|
+
}
|
|
621
|
+
this.distribution = new cloudfront2.Distribution(this, "Distribution", distributionProps);
|
|
622
|
+
resolveAssetDeployment(this, "", {
|
|
623
|
+
path: props.path,
|
|
624
|
+
bucket: this.bucket,
|
|
625
|
+
fileOptions,
|
|
626
|
+
distribution: this.distribution,
|
|
627
|
+
invalidation: props.invalidation
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/react-router-ssr-site-lambda.ts
|
|
633
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
634
|
+
import { Construct as Construct9 } from "constructs";
|
|
635
|
+
|
|
636
|
+
// src/helpers/ssr-site-lambda.ts
|
|
637
|
+
import "constructs";
|
|
638
|
+
import { Duration as Duration3, RemovalPolicy as RemovalPolicy3, Stack as Stack2, SymlinkFollowMode } from "aws-cdk-lib";
|
|
639
|
+
import * as lambda2 from "aws-cdk-lib/aws-lambda";
|
|
640
|
+
import * as s34 from "aws-cdk-lib/aws-s3";
|
|
641
|
+
import * as cloudfront3 from "aws-cdk-lib/aws-cloudfront";
|
|
642
|
+
import * as origins2 from "aws-cdk-lib/aws-cloudfront-origins";
|
|
643
|
+
import * as iam4 from "aws-cdk-lib/aws-iam";
|
|
644
|
+
|
|
645
|
+
// src/lambda-function.ts
|
|
646
|
+
import { Construct as Construct7 } from "constructs";
|
|
647
|
+
import { Duration as Duration2, Tags } from "aws-cdk-lib";
|
|
648
|
+
import * as lambda from "aws-cdk-lib/aws-lambda";
|
|
649
|
+
import * as lambdaNode from "aws-cdk-lib/aws-lambda-nodejs";
|
|
650
|
+
import * as logs3 from "aws-cdk-lib/aws-logs";
|
|
651
|
+
import * as iam3 from "aws-cdk-lib/aws-iam";
|
|
652
|
+
var ESM_BANNER = [
|
|
653
|
+
'import { createRequire as __banner_require__ } from "module";',
|
|
654
|
+
'import { fileURLToPath as __banner_fileURLToPath__ } from "url";',
|
|
655
|
+
'import { dirname as __banner_dirname__ } from "path";',
|
|
656
|
+
"const require = __banner_require__(import.meta.url);",
|
|
657
|
+
"const __filename = __banner_fileURLToPath__(import.meta.url);",
|
|
658
|
+
"const __dirname = __banner_dirname__(__filename);"
|
|
659
|
+
].join(" ");
|
|
660
|
+
var LambdaFunction = class extends Construct7 {
|
|
661
|
+
function;
|
|
662
|
+
logGroup;
|
|
663
|
+
url;
|
|
664
|
+
version;
|
|
665
|
+
alias;
|
|
666
|
+
constructor(scope, id, props) {
|
|
667
|
+
super(scope, id);
|
|
668
|
+
if (props.bundle && props.code) {
|
|
669
|
+
throw new Error('Function: cannot set both "bundle" and "code" \u2014 use one or the other');
|
|
670
|
+
}
|
|
671
|
+
if (props.streaming && !props.url) {
|
|
672
|
+
throw new Error('Function: "streaming" requires "url" to be enabled');
|
|
673
|
+
}
|
|
674
|
+
const code = props.bundle ? lambda.Code.fromAsset(props.bundle) : props.code;
|
|
675
|
+
const runtime = props.runtime ?? lambda.Runtime.NODEJS_22_X;
|
|
676
|
+
const architecture = props.architecture ?? lambda.Architecture.ARM_64;
|
|
677
|
+
const memorySize = props.memory ?? 1024;
|
|
678
|
+
const timeout = props.timeout ?? Duration2.seconds(20);
|
|
679
|
+
const loggingFormat = props.logging?.format === "json" ? lambda.LoggingFormat.JSON : lambda.LoggingFormat.TEXT;
|
|
680
|
+
let logGroupProps = {
|
|
681
|
+
retention: resolveLogRetention(props.logging?.retention)
|
|
682
|
+
};
|
|
683
|
+
if (props.transform?.logGroup) {
|
|
684
|
+
logGroupProps = props.transform.logGroup(logGroupProps);
|
|
685
|
+
}
|
|
686
|
+
this.logGroup = new logs3.LogGroup(this, "LogGroup", logGroupProps);
|
|
687
|
+
const vpcConfig = resolveVpc(props.vpc);
|
|
688
|
+
if (code) {
|
|
689
|
+
let fnProps = {
|
|
690
|
+
code,
|
|
691
|
+
handler: props.handler,
|
|
692
|
+
runtime,
|
|
693
|
+
architecture,
|
|
694
|
+
memorySize,
|
|
695
|
+
timeout,
|
|
696
|
+
ephemeralStorageSize: props.storage,
|
|
697
|
+
description: props.description,
|
|
698
|
+
environment: props.environment,
|
|
699
|
+
layers: props.layers,
|
|
700
|
+
logGroup: this.logGroup,
|
|
701
|
+
loggingFormat,
|
|
702
|
+
vpc: vpcConfig.vpc,
|
|
703
|
+
vpcSubnets: vpcConfig.subnets,
|
|
704
|
+
securityGroups: vpcConfig.securityGroups
|
|
705
|
+
};
|
|
706
|
+
if (props.transform?.function) {
|
|
707
|
+
fnProps = props.transform.function(fnProps);
|
|
708
|
+
}
|
|
709
|
+
this.function = new lambda.Function(this, "Function", fnProps);
|
|
710
|
+
} else {
|
|
711
|
+
const parsed = parseHandler(props.handler);
|
|
712
|
+
const entry = resolveEntry(parsed.entry);
|
|
713
|
+
const nodejsOpts = props.nodejs ?? {};
|
|
714
|
+
const format = nodejsOpts.format ?? lambdaNode.OutputFormat.ESM;
|
|
715
|
+
const isEsm = format === lambdaNode.OutputFormat.ESM;
|
|
716
|
+
const banner = nodejsOpts.banner ?? (isEsm ? ESM_BANNER : void 0);
|
|
717
|
+
const defaultExclude = ["@aws-sdk/*"];
|
|
718
|
+
const exclude = nodejsOpts.exclude ?? defaultExclude;
|
|
719
|
+
const install = nodejsOpts.install ?? [];
|
|
720
|
+
const afterBundlingCommands = [];
|
|
721
|
+
if (nodejsOpts.copyFiles) {
|
|
722
|
+
afterBundlingCommands.push(...resolveCopyFiles(nodejsOpts.copyFiles));
|
|
723
|
+
}
|
|
724
|
+
const hasCommandHooks = afterBundlingCommands.length > 0 || props.hook?.postBuild;
|
|
725
|
+
let fnProps = {
|
|
726
|
+
entry,
|
|
727
|
+
handler: parsed.export,
|
|
728
|
+
runtime,
|
|
729
|
+
architecture,
|
|
730
|
+
memorySize,
|
|
731
|
+
timeout,
|
|
732
|
+
ephemeralStorageSize: props.storage,
|
|
733
|
+
description: props.description,
|
|
734
|
+
environment: props.environment,
|
|
735
|
+
layers: props.layers,
|
|
736
|
+
logGroup: this.logGroup,
|
|
737
|
+
loggingFormat,
|
|
738
|
+
vpc: vpcConfig.vpc,
|
|
739
|
+
vpcSubnets: vpcConfig.subnets,
|
|
740
|
+
securityGroups: vpcConfig.securityGroups,
|
|
741
|
+
bundling: {
|
|
742
|
+
minify: nodejsOpts.minify ?? true,
|
|
743
|
+
sourceMap: nodejsOpts.sourceMap ?? false,
|
|
744
|
+
format,
|
|
745
|
+
target: "es2023",
|
|
746
|
+
banner,
|
|
747
|
+
externalModules: [...exclude, ...install],
|
|
748
|
+
nodeModules: install.length > 0 ? install : void 0,
|
|
749
|
+
loader: nodejsOpts.loader,
|
|
750
|
+
esbuildArgs: nodejsOpts.esbuild,
|
|
751
|
+
commandHooks: hasCommandHooks ? {
|
|
752
|
+
beforeBundling: () => [],
|
|
753
|
+
beforeInstall: () => [],
|
|
754
|
+
afterBundling: (_inputDir, outputDir) => [
|
|
755
|
+
...afterBundlingCommands.map((cmd) => cmd.replace("$out", outputDir)),
|
|
756
|
+
...props.hook?.postBuild?.(outputDir) ?? []
|
|
757
|
+
]
|
|
758
|
+
} : void 0
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
if (props.transform?.function) {
|
|
762
|
+
fnProps = props.transform.function(fnProps);
|
|
763
|
+
}
|
|
764
|
+
this.function = new lambdaNode.NodejsFunction(this, "Function", fnProps);
|
|
765
|
+
}
|
|
766
|
+
if (props.permissions) {
|
|
767
|
+
for (const perm of props.permissions) {
|
|
768
|
+
this.function.addToRolePolicy(new iam3.PolicyStatement({ actions: perm.actions, resources: perm.resources }));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (props.policies) {
|
|
772
|
+
for (const arn of props.policies) {
|
|
773
|
+
this.function.role.addManagedPolicy(iam3.ManagedPolicy.fromManagedPolicyArn(this, `Policy-${arn}`, arn));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (props.retries !== void 0) {
|
|
777
|
+
new lambda.EventInvokeConfig(this, "EventInvokeConfig", {
|
|
778
|
+
function: this.function,
|
|
779
|
+
retryAttempts: props.retries
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (props.versioning || props.concurrency?.provisioned) {
|
|
783
|
+
;
|
|
784
|
+
this.version = this.function.currentVersion;
|
|
785
|
+
}
|
|
786
|
+
if (props.concurrency?.reserved !== void 0) {
|
|
787
|
+
const cfnFunction = this.function.node.defaultChild;
|
|
788
|
+
cfnFunction.addPropertyOverride("ReservedConcurrentExecutions", props.concurrency.reserved);
|
|
789
|
+
}
|
|
790
|
+
if (props.concurrency?.provisioned) {
|
|
791
|
+
const version = this.version ?? this.function.currentVersion;
|
|
792
|
+
const alias = new lambda.Alias(this, "Alias", {
|
|
793
|
+
aliasName: "live",
|
|
794
|
+
version,
|
|
795
|
+
provisionedConcurrentExecutions: props.concurrency.provisioned
|
|
796
|
+
});
|
|
797
|
+
this.version = version;
|
|
798
|
+
this.alias = alias;
|
|
799
|
+
}
|
|
800
|
+
if (props.tags) {
|
|
801
|
+
for (const [key, value] of Object.entries(props.tags)) {
|
|
802
|
+
Tags.of(this).add(key, value);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (props.url) {
|
|
806
|
+
const urlOpts = resolveUrl(props.url);
|
|
807
|
+
this.url = this.function.addFunctionUrl({
|
|
808
|
+
authType: urlOpts.authorization === "iam" ? lambda.FunctionUrlAuthType.AWS_IAM : lambda.FunctionUrlAuthType.NONE,
|
|
809
|
+
cors: urlOpts.cors === true ? { allowedOrigins: ["*"], allowedMethods: [lambda.HttpMethod.ALL] } : urlOpts.cors,
|
|
810
|
+
invokeMode: props.streaming ? lambda.InvokeMode.RESPONSE_STREAM : void 0
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function parseHandler(handler) {
|
|
816
|
+
const lastDot = handler.lastIndexOf(".");
|
|
817
|
+
if (lastDot === -1) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`Function: handler "${handler}" must contain a dot separating the file path from the export name (e.g. "src/api.handler")`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
entry: handler.slice(0, lastDot),
|
|
824
|
+
export: handler.slice(lastDot + 1)
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function resolveEntry(filePath) {
|
|
828
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
829
|
+
const basename = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
|
|
830
|
+
if (basename.includes(".")) return filePath;
|
|
831
|
+
return filePath + ".ts";
|
|
832
|
+
}
|
|
833
|
+
function resolveVpc(vpc) {
|
|
834
|
+
if (!vpc) return {};
|
|
835
|
+
if ("vpc" in vpc) return vpc;
|
|
836
|
+
return { vpc };
|
|
837
|
+
}
|
|
838
|
+
function resolveUrl(url) {
|
|
839
|
+
if (url === true) return { authorization: "none" };
|
|
840
|
+
if (!url) return { authorization: "none" };
|
|
841
|
+
return {
|
|
842
|
+
authorization: url.authorization ?? "none",
|
|
843
|
+
cors: url.cors
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function resolveCopyFiles(copyFiles) {
|
|
847
|
+
return copyFiles.map((f) => {
|
|
848
|
+
const dest = f.to ?? f.from;
|
|
849
|
+
return `cp -r ${f.from} $out/${dest}`;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/helpers/ssr-site-lambda.ts
|
|
854
|
+
var defaultAssetBehaviors = [{ pathPattern: "/_assets/*", cachePolicy: "CACHING_OPTIMIZED" }];
|
|
855
|
+
function createSsrSiteLambda(scope, props) {
|
|
856
|
+
if (props.server.nodejs && props.server.code) {
|
|
857
|
+
throw new Error("SsrSiteLambda: server.nodejs and server.code are mutually exclusive");
|
|
858
|
+
}
|
|
859
|
+
if (!props.server.code && !props.server.path) {
|
|
860
|
+
throw new Error("SsrSiteLambda: either server.code or server.path must be provided");
|
|
861
|
+
}
|
|
862
|
+
const useBundling = !!props.server.nodejs;
|
|
863
|
+
const assetBehaviors = props.assets?.behaviors ?? defaultAssetBehaviors;
|
|
864
|
+
const handler = props.server.handler ?? "index.handler";
|
|
865
|
+
const serverCode = useBundling ? void 0 : props.server.code ?? lambda2.Code.fromAsset(props.server.path, { followSymlinks: SymlinkFollowMode.ALWAYS });
|
|
866
|
+
const fn = new LambdaFunction(scope, "Server", {
|
|
867
|
+
code: serverCode,
|
|
868
|
+
handler: useBundling ? resolveBundlingHandler(handler, props.server.path) : handler,
|
|
869
|
+
nodejs: useBundling ? props.server.nodejs : void 0,
|
|
870
|
+
runtime: props.server.runtime ?? lambda2.Runtime.NODEJS_22_X,
|
|
871
|
+
memory: props.server.memory ?? 1024,
|
|
872
|
+
timeout: props.server.timeout ?? Duration3.seconds(30),
|
|
873
|
+
architecture: props.server.architecture ?? lambda2.Architecture.ARM_64,
|
|
874
|
+
environment: props.environment,
|
|
875
|
+
vpc: props.vpc,
|
|
876
|
+
permissions: props.permissions,
|
|
877
|
+
layers: props.server.layers,
|
|
878
|
+
streaming: props.streaming,
|
|
879
|
+
url: { authorization: "iam" },
|
|
880
|
+
transform: props.transform?.lambda ? { function: props.transform.lambda } : void 0
|
|
881
|
+
});
|
|
882
|
+
const fnResource = fn.function;
|
|
883
|
+
const functionUrl = fn.url;
|
|
884
|
+
let bucketProps = {
|
|
885
|
+
blockPublicAccess: s34.BlockPublicAccess.BLOCK_ALL,
|
|
886
|
+
removalPolicy: RemovalPolicy3.RETAIN
|
|
887
|
+
};
|
|
888
|
+
if (props.transform?.bucket) {
|
|
889
|
+
bucketProps = props.transform.bucket(bucketProps);
|
|
890
|
+
}
|
|
891
|
+
const bucket = new s34.Bucket(scope, "Bucket", bucketProps);
|
|
892
|
+
const cfFunction = props.edge?.viewerRequest ?? createViewerRequestFunction(scope, "ViewerRequestFn");
|
|
893
|
+
const defaultFunctionAssociations = [
|
|
894
|
+
{ function: cfFunction, eventType: cloudfront3.FunctionEventType.VIEWER_REQUEST }
|
|
895
|
+
];
|
|
896
|
+
if (props.edge?.viewerResponse) {
|
|
897
|
+
defaultFunctionAssociations.push({
|
|
898
|
+
function: props.edge.viewerResponse,
|
|
899
|
+
eventType: cloudfront3.FunctionEventType.VIEWER_RESPONSE
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const { certificate, domainNames } = resolveCertificate(scope, props.domain);
|
|
903
|
+
const s3Origin = origins2.S3BucketOrigin.withOriginAccessControl(bucket);
|
|
904
|
+
const lambdaOrigin = origins2.FunctionUrlOrigin.withOriginAccessControl(functionUrl);
|
|
905
|
+
const additionalBehaviors = resolveAdditionalBehaviors(s3Origin, assetBehaviors);
|
|
906
|
+
let distributionProps = {
|
|
907
|
+
defaultBehavior: {
|
|
908
|
+
origin: lambdaOrigin,
|
|
909
|
+
viewerProtocolPolicy: cloudfront3.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
910
|
+
cachePolicy: cloudfront3.CachePolicy.CACHING_DISABLED,
|
|
911
|
+
originRequestPolicy: cloudfront3.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
912
|
+
allowedMethods: cloudfront3.AllowedMethods.ALLOW_ALL,
|
|
913
|
+
functionAssociations: defaultFunctionAssociations
|
|
914
|
+
},
|
|
915
|
+
additionalBehaviors,
|
|
916
|
+
domainNames,
|
|
917
|
+
certificate
|
|
918
|
+
};
|
|
919
|
+
if (props.transform?.distribution) {
|
|
920
|
+
distributionProps = props.transform.distribution(distributionProps);
|
|
921
|
+
}
|
|
922
|
+
const distribution = new cloudfront3.Distribution(scope, "Distribution", distributionProps);
|
|
923
|
+
fnResource.addPermission("CloudFrontInvokeFunction", {
|
|
924
|
+
principal: new iam4.ServicePrincipal("cloudfront.amazonaws.com"),
|
|
925
|
+
action: "lambda:InvokeFunction",
|
|
926
|
+
sourceArn: `arn:aws:cloudfront::${Stack2.of(scope).account}:distribution/${distribution.distributionId}`
|
|
927
|
+
});
|
|
928
|
+
bucket.grantRead(fnResource);
|
|
929
|
+
if (props.assets?.path) {
|
|
930
|
+
const fileOptions = props.assets.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }];
|
|
931
|
+
resolveAssetDeployment(scope, "", {
|
|
932
|
+
path: props.assets.path,
|
|
933
|
+
bucket,
|
|
934
|
+
prefix: props.assets.prefix,
|
|
935
|
+
fileOptions,
|
|
936
|
+
distribution,
|
|
937
|
+
invalidation: props.invalidation
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return { lambda: fnResource, functionUrl, bucket, distribution };
|
|
941
|
+
}
|
|
942
|
+
function resolveBundlingHandler(handler, serverPath) {
|
|
943
|
+
const lastDot = handler.lastIndexOf(".");
|
|
944
|
+
if (lastDot === -1) return `${serverPath}/${handler}`;
|
|
945
|
+
const file = handler.slice(0, lastDot);
|
|
946
|
+
const exportName = handler.slice(lastDot + 1);
|
|
947
|
+
return `${serverPath}/${file}.js.${exportName}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/react-router-ssr-site-lambda.ts
|
|
951
|
+
var ReactRouterSsrSiteLambda = class extends Construct9 {
|
|
952
|
+
lambda;
|
|
953
|
+
functionUrl;
|
|
954
|
+
bucket;
|
|
955
|
+
distribution;
|
|
956
|
+
constructor(scope, id, props) {
|
|
957
|
+
super(scope, id);
|
|
958
|
+
const basePath = !props.path || props.path === "." ? props.buildDirectory ?? "build" : `${props.path}/${props.buildDirectory ?? "build"}`;
|
|
959
|
+
const serverBuild = `${basePath}/server/index.js`;
|
|
960
|
+
if (!existsSync2(serverBuild)) {
|
|
961
|
+
throw new Error(`ReactRouterSsrSiteLambda: "${serverBuild}" not found. Run "react-router build" before deploying.`);
|
|
962
|
+
}
|
|
963
|
+
const mode = props.server?.mode ?? "streaming";
|
|
964
|
+
const handlerSource = buildHandlerSource("./server/index.js", mode);
|
|
965
|
+
writeFileSync2(`${basePath}/_lambda-handler.js`, handlerSource);
|
|
966
|
+
const external = props.server?.external ?? [];
|
|
967
|
+
const merged = {
|
|
968
|
+
streaming: mode === "streaming",
|
|
969
|
+
environment: props.environment,
|
|
970
|
+
vpc: props.vpc,
|
|
971
|
+
permissions: props.permissions,
|
|
972
|
+
domain: props.domain,
|
|
973
|
+
edge: props.edge,
|
|
974
|
+
invalidation: props.invalidation,
|
|
975
|
+
transform: props.transform,
|
|
976
|
+
server: {
|
|
977
|
+
path: basePath,
|
|
978
|
+
handler: "_lambda-handler.handler",
|
|
979
|
+
runtime: props.server?.runtime,
|
|
980
|
+
memory: props.server?.memory,
|
|
981
|
+
timeout: props.server?.timeout,
|
|
982
|
+
architecture: props.server?.architecture,
|
|
983
|
+
layers: props.server?.layers,
|
|
984
|
+
nodejs: {
|
|
985
|
+
install: props.server?.install,
|
|
986
|
+
exclude: ["@aws-sdk/*", ...external]
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
assets: {
|
|
990
|
+
...props.assets,
|
|
991
|
+
path: props.assets?.path ?? `${basePath}/client`,
|
|
992
|
+
behaviors: props.assets?.behaviors ?? [{ pathPattern: "/assets/*", cachePolicy: "CACHING_OPTIMIZED" }],
|
|
993
|
+
fileOptions: props.assets?.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }]
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
const resources = createSsrSiteLambda(this, merged);
|
|
997
|
+
this.lambda = resources.lambda;
|
|
998
|
+
this.functionUrl = resources.functionUrl;
|
|
999
|
+
this.bucket = resources.bucket;
|
|
1000
|
+
this.distribution = resources.distribution;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function buildHandlerSource(serverBuildPath, mode) {
|
|
1004
|
+
const shared = `
|
|
1005
|
+
import { createRequestHandler } from 'react-router'
|
|
1006
|
+
import * as build from '${serverBuildPath}'
|
|
1007
|
+
|
|
1008
|
+
const handleRequest = createRequestHandler(build, process.env.NODE_ENV)
|
|
1009
|
+
|
|
1010
|
+
function createRequest(event) {
|
|
1011
|
+
var host = event.headers['x-forwarded-host'] || event.headers.host
|
|
1012
|
+
var search = event.rawQueryString.length ? '?' + event.rawQueryString : ''
|
|
1013
|
+
var scheme = event.headers['x-forwarded-proto'] || 'http'
|
|
1014
|
+
var url = new URL(event.rawPath + search, scheme + '://' + host)
|
|
1015
|
+
var isFormData = event.headers['content-type']?.includes('multipart/form-data')
|
|
1016
|
+
|
|
1017
|
+
return new Request(url.href, {
|
|
1018
|
+
method: event.requestContext.http.method,
|
|
1019
|
+
headers: createHeaders(event.headers, event.cookies),
|
|
1020
|
+
signal: new AbortController().signal,
|
|
1021
|
+
body:
|
|
1022
|
+
event.body && event.isBase64Encoded
|
|
1023
|
+
? isFormData
|
|
1024
|
+
? Buffer.from(event.body, 'base64')
|
|
1025
|
+
: Buffer.from(event.body, 'base64').toString()
|
|
1026
|
+
: event.body
|
|
1027
|
+
})
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function createHeaders(requestHeaders, requestCookies) {
|
|
1031
|
+
var headers = new Headers()
|
|
1032
|
+
for (var [header, value] of Object.entries(requestHeaders)) {
|
|
1033
|
+
if (value) {
|
|
1034
|
+
headers.append(header, value)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (requestCookies) {
|
|
1038
|
+
headers.append('Cookie', requestCookies.join('; '))
|
|
1039
|
+
}
|
|
1040
|
+
return headers
|
|
1041
|
+
}
|
|
1042
|
+
`;
|
|
1043
|
+
if (mode === "streaming") {
|
|
1044
|
+
return shared + `
|
|
1045
|
+
async function writeReadableStreamToWritable(readableStream, writable) {
|
|
1046
|
+
var reader = readableStream.getReader()
|
|
1047
|
+
try {
|
|
1048
|
+
while (true) {
|
|
1049
|
+
var { done, value } = await reader.read()
|
|
1050
|
+
if (done) break
|
|
1051
|
+
writable.write(value)
|
|
1052
|
+
}
|
|
1053
|
+
} finally {
|
|
1054
|
+
reader.releaseLock()
|
|
1055
|
+
writable.end()
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async function sendResponse(response, responseStream) {
|
|
1060
|
+
var cookies = response.headers.getSetCookie()
|
|
1061
|
+
if (cookies.length) {
|
|
1062
|
+
response.headers.delete('Set-Cookie')
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
var metadata = {
|
|
1066
|
+
statusCode: response.status,
|
|
1067
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1068
|
+
cookies: cookies
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
var body =
|
|
1072
|
+
response.body ??
|
|
1073
|
+
new ReadableStream({
|
|
1074
|
+
start(controller) {
|
|
1075
|
+
controller.enqueue(new Uint8Array(0))
|
|
1076
|
+
controller.close()
|
|
1077
|
+
}
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
var httpResponseStream = awslambda.HttpResponseStream.from(responseStream, metadata)
|
|
1081
|
+
await writeReadableStreamToWritable(body, httpResponseStream)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export var handler = awslambda.streamifyResponse(async function(event, responseStream) {
|
|
1085
|
+
var request
|
|
1086
|
+
try {
|
|
1087
|
+
request = createRequest(event)
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
return await sendResponse(
|
|
1090
|
+
new Response('Bad Request: ' + (e instanceof Error ? e.message : e), { status: 400 }),
|
|
1091
|
+
responseStream
|
|
1092
|
+
)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
var response = await handleRequest(request)
|
|
1096
|
+
return await sendResponse(response, responseStream)
|
|
1097
|
+
})
|
|
1098
|
+
`;
|
|
1099
|
+
}
|
|
1100
|
+
return shared + `
|
|
1101
|
+
async function buildBufferedResponse(response) {
|
|
1102
|
+
var cookies = response.headers.getSetCookie()
|
|
1103
|
+
if (cookies.length) {
|
|
1104
|
+
response.headers.delete('Set-Cookie')
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
var contentType = response.headers.get('Content-Type')
|
|
1108
|
+
var isBinary = contentType && !contentType.startsWith('text/') && !contentType.includes('json')
|
|
1109
|
+
|
|
1110
|
+
var body
|
|
1111
|
+
var isBase64Encoded = false
|
|
1112
|
+
|
|
1113
|
+
if (isBinary) {
|
|
1114
|
+
var arrayBuffer = await response.arrayBuffer()
|
|
1115
|
+
body = Buffer.from(arrayBuffer).toString('base64')
|
|
1116
|
+
isBase64Encoded = true
|
|
1117
|
+
} else {
|
|
1118
|
+
body = await response.text()
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return {
|
|
1122
|
+
statusCode: response.status,
|
|
1123
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1124
|
+
cookies: cookies,
|
|
1125
|
+
body: body,
|
|
1126
|
+
isBase64Encoded: isBase64Encoded
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export async function handler(event) {
|
|
1131
|
+
var request
|
|
1132
|
+
try {
|
|
1133
|
+
request = createRequest(event)
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
return buildBufferedResponse(
|
|
1136
|
+
new Response('Bad Request: ' + (e instanceof Error ? e.message : e), { status: 400 })
|
|
1137
|
+
)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
var response = await handleRequest(request)
|
|
1141
|
+
return buildBufferedResponse(response)
|
|
1142
|
+
}
|
|
1143
|
+
`;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/react-router-ssr-site-ecs-fargate-service.ts
|
|
1147
|
+
import { Construct as Construct12 } from "constructs";
|
|
1148
|
+
|
|
1149
|
+
// src/helpers/ssr-site-ecs-service.ts
|
|
1150
|
+
import "constructs";
|
|
1151
|
+
import { RemovalPolicy as RemovalPolicy4 } from "aws-cdk-lib";
|
|
1152
|
+
import * as s35 from "aws-cdk-lib/aws-s3";
|
|
1153
|
+
import * as cloudfront4 from "aws-cdk-lib/aws-cloudfront";
|
|
1154
|
+
import * as origins3 from "aws-cdk-lib/aws-cloudfront-origins";
|
|
1155
|
+
import * as iam6 from "aws-cdk-lib/aws-iam";
|
|
1156
|
+
|
|
1157
|
+
// src/ecs-fargate-service.ts
|
|
1158
|
+
import { Construct as Construct10 } from "constructs";
|
|
1159
|
+
import { Duration as Duration4 } from "aws-cdk-lib";
|
|
1160
|
+
import * as ec22 from "aws-cdk-lib/aws-ec2";
|
|
1161
|
+
import * as ecs3 from "aws-cdk-lib/aws-ecs";
|
|
1162
|
+
import * as ecrAssets from "aws-cdk-lib/aws-ecr-assets";
|
|
1163
|
+
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
1164
|
+
import * as logs4 from "aws-cdk-lib/aws-logs";
|
|
1165
|
+
import * as iam5 from "aws-cdk-lib/aws-iam";
|
|
1166
|
+
import * as acm2 from "aws-cdk-lib/aws-certificatemanager";
|
|
1167
|
+
var EcsFargateService = class extends Construct10 {
|
|
1168
|
+
service;
|
|
1169
|
+
taskDefinition;
|
|
1170
|
+
containers;
|
|
1171
|
+
logGroups;
|
|
1172
|
+
securityGroup;
|
|
1173
|
+
alb;
|
|
1174
|
+
listener;
|
|
1175
|
+
targetGroups;
|
|
1176
|
+
constructor(scope, id, props) {
|
|
1177
|
+
super(scope, id);
|
|
1178
|
+
if (!props.containers.length) {
|
|
1179
|
+
throw new Error("At least one container is required");
|
|
1180
|
+
}
|
|
1181
|
+
const cpu = props.cpu ?? 256;
|
|
1182
|
+
const memoryLimitMiB = props.memoryLimitMiB ?? 512;
|
|
1183
|
+
const arch = props.architecture ?? "X86_64";
|
|
1184
|
+
const cpuArchitecture = arch === "ARM64" ? ecs3.CpuArchitecture.ARM64 : ecs3.CpuArchitecture.X86_64;
|
|
1185
|
+
const platform = arch === "ARM64" ? ecrAssets.Platform.LINUX_ARM64 : ecrAssets.Platform.LINUX_AMD64;
|
|
1186
|
+
let executionRoleProps = {
|
|
1187
|
+
assumedBy: new iam5.ServicePrincipal("ecs-tasks.amazonaws.com"),
|
|
1188
|
+
managedPolicies: [iam5.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")]
|
|
1189
|
+
};
|
|
1190
|
+
if (props.transform?.executionRole) {
|
|
1191
|
+
executionRoleProps = props.transform.executionRole(executionRoleProps);
|
|
1192
|
+
}
|
|
1193
|
+
const executionRole = new iam5.Role(this, "ExecutionRole", executionRoleProps);
|
|
1194
|
+
let taskRoleProps = {
|
|
1195
|
+
assumedBy: new iam5.ServicePrincipal("ecs-tasks.amazonaws.com")
|
|
1196
|
+
};
|
|
1197
|
+
if (props.transform?.taskRole) {
|
|
1198
|
+
taskRoleProps = props.transform.taskRole(taskRoleProps);
|
|
1199
|
+
}
|
|
1200
|
+
const taskRole = new iam5.Role(this, "TaskRole", taskRoleProps);
|
|
1201
|
+
let taskDefProps = {
|
|
1202
|
+
cpu,
|
|
1203
|
+
memoryLimitMiB,
|
|
1204
|
+
executionRole,
|
|
1205
|
+
taskRole,
|
|
1206
|
+
runtimePlatform: {
|
|
1207
|
+
cpuArchitecture,
|
|
1208
|
+
operatingSystemFamily: ecs3.OperatingSystemFamily.LINUX
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
if (props.transform?.taskDefinition) {
|
|
1212
|
+
taskDefProps = props.transform.taskDefinition(taskDefProps);
|
|
1213
|
+
}
|
|
1214
|
+
this.taskDefinition = new ecs3.FargateTaskDefinition(this, "TaskDefinition", taskDefProps);
|
|
1215
|
+
if (props.permissions) {
|
|
1216
|
+
for (const perm of props.permissions) {
|
|
1217
|
+
this.taskDefinition.taskRole.addToPrincipalPolicy(
|
|
1218
|
+
new iam5.PolicyStatement({
|
|
1219
|
+
actions: perm.actions,
|
|
1220
|
+
resources: perm.resources,
|
|
1221
|
+
effect: perm.effect === "deny" ? iam5.Effect.DENY : iam5.Effect.ALLOW,
|
|
1222
|
+
conditions: perm.conditions
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
this.containers = {};
|
|
1228
|
+
this.logGroups = {};
|
|
1229
|
+
for (const c of props.containers) {
|
|
1230
|
+
const name = resolveContainerName(c, props.containers);
|
|
1231
|
+
if (!name) {
|
|
1232
|
+
throw new Error("All containers must have a name when multiple containers are defined");
|
|
1233
|
+
}
|
|
1234
|
+
const containerImage = resolveImage(this, c.image, platform, name);
|
|
1235
|
+
const resolvedSecrets = resolveSecrets(this, c.ssm, c.secrets, `${name}`);
|
|
1236
|
+
let logGroupProps = {
|
|
1237
|
+
retention: resolveLogRetention(c.logging?.retention)
|
|
1238
|
+
};
|
|
1239
|
+
if (props.transform?.logGroup) {
|
|
1240
|
+
logGroupProps = props.transform.logGroup(logGroupProps);
|
|
1241
|
+
}
|
|
1242
|
+
const logGroup = new logs4.LogGroup(this, `${name}LogGroup`, logGroupProps);
|
|
1243
|
+
this.logGroups[name] = logGroup;
|
|
1244
|
+
let containerProps = {
|
|
1245
|
+
image: containerImage,
|
|
1246
|
+
logging: ecs3.LogDrivers.awsLogs({ streamPrefix: `${id}-${name}`, logGroup }),
|
|
1247
|
+
environment: c.environment,
|
|
1248
|
+
secrets: resolvedSecrets,
|
|
1249
|
+
command: c.command,
|
|
1250
|
+
entryPoint: c.entrypoint,
|
|
1251
|
+
portMappings: c.port ? [{ containerPort: c.port }] : void 0,
|
|
1252
|
+
cpu: c.cpu,
|
|
1253
|
+
memoryLimitMiB: c.memory,
|
|
1254
|
+
healthCheck: c.health ? {
|
|
1255
|
+
command: c.health.command,
|
|
1256
|
+
interval: c.health.interval ? Duration4.seconds(c.health.interval) : void 0,
|
|
1257
|
+
timeout: c.health.timeout ? Duration4.seconds(c.health.timeout) : void 0,
|
|
1258
|
+
retries: c.health.retries,
|
|
1259
|
+
startPeriod: c.health.startPeriod ? Duration4.seconds(c.health.startPeriod) : void 0
|
|
1260
|
+
} : void 0
|
|
1261
|
+
};
|
|
1262
|
+
if (props.transform?.container) {
|
|
1263
|
+
containerProps = props.transform.container(containerProps);
|
|
1264
|
+
}
|
|
1265
|
+
this.containers[name] = this.taskDefinition.addContainer(name, containerProps);
|
|
1266
|
+
}
|
|
1267
|
+
this.securityGroup = new ec22.SecurityGroup(this, "ServiceSecurityGroup", {
|
|
1268
|
+
vpc: props.cluster.vpc,
|
|
1269
|
+
description: `ECS Service SG for ${id}`,
|
|
1270
|
+
allowAllOutbound: true
|
|
1271
|
+
});
|
|
1272
|
+
if (props.loadBalancer) {
|
|
1273
|
+
const lb = props.loadBalancer;
|
|
1274
|
+
const isPublic = lb.public !== false;
|
|
1275
|
+
const rules = lb.rules ?? autoRules(props.containers, !!lb.domain);
|
|
1276
|
+
const resolvedRules = rules.map((rule) => ({
|
|
1277
|
+
...rule,
|
|
1278
|
+
container: rule.container ? rule.container : resolveDefaultContainerName(props.containers)
|
|
1279
|
+
}));
|
|
1280
|
+
for (const rule of resolvedRules) {
|
|
1281
|
+
if (!rule.redirect && !this.containers[rule.container]) {
|
|
1282
|
+
throw new Error(`Routing rule references unknown container '${rule.container}'`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
let albSgProps = {
|
|
1286
|
+
vpc: props.cluster.vpc,
|
|
1287
|
+
description: `ALB SG for ${id}`,
|
|
1288
|
+
allowAllOutbound: true
|
|
1289
|
+
};
|
|
1290
|
+
if (props.transform?.albSecurityGroup) {
|
|
1291
|
+
albSgProps = props.transform.albSecurityGroup(albSgProps);
|
|
1292
|
+
}
|
|
1293
|
+
const albSg = new ec22.SecurityGroup(this, "AlbSecurityGroup", albSgProps);
|
|
1294
|
+
let albProps = {
|
|
1295
|
+
vpc: props.cluster.vpc,
|
|
1296
|
+
internetFacing: isPublic,
|
|
1297
|
+
securityGroup: albSg
|
|
1298
|
+
};
|
|
1299
|
+
if (props.transform?.alb) {
|
|
1300
|
+
albProps = props.transform.alb(albProps);
|
|
1301
|
+
}
|
|
1302
|
+
this.alb = new elbv2.ApplicationLoadBalancer(this, "Alb", albProps);
|
|
1303
|
+
const tgMap = {};
|
|
1304
|
+
for (const rule of resolvedRules) {
|
|
1305
|
+
if (rule.redirect) continue;
|
|
1306
|
+
const forwardPort = resolveForwardPort(rule, props.containers);
|
|
1307
|
+
const tgKey = `${rule.container}:${forwardPort}`;
|
|
1308
|
+
if (tgMap[tgKey]) continue;
|
|
1309
|
+
const health = lb.health;
|
|
1310
|
+
let targetGroupProps = {
|
|
1311
|
+
vpc: props.cluster.vpc,
|
|
1312
|
+
port: forwardPort,
|
|
1313
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
1314
|
+
targetType: elbv2.TargetType.IP,
|
|
1315
|
+
deregistrationDelay: Duration4.seconds(30),
|
|
1316
|
+
...health ? {
|
|
1317
|
+
healthCheck: {
|
|
1318
|
+
path: health.path ?? "/health",
|
|
1319
|
+
interval: health.interval ? Duration4.seconds(health.interval) : void 0,
|
|
1320
|
+
timeout: health.timeout ? Duration4.seconds(health.timeout) : void 0,
|
|
1321
|
+
healthyThresholdCount: health.healthyThreshold,
|
|
1322
|
+
unhealthyThresholdCount: health.unhealthyThreshold,
|
|
1323
|
+
healthyHttpCodes: health.successCodes
|
|
1324
|
+
}
|
|
1325
|
+
} : {}
|
|
1326
|
+
};
|
|
1327
|
+
if (props.transform?.targetGroup) {
|
|
1328
|
+
targetGroupProps = props.transform.targetGroup(targetGroupProps);
|
|
1329
|
+
}
|
|
1330
|
+
tgMap[tgKey] = new elbv2.ApplicationTargetGroup(this, `${rule.container}Port${forwardPort}Tg`, targetGroupProps);
|
|
1331
|
+
}
|
|
1332
|
+
this.targetGroups = tgMap;
|
|
1333
|
+
const listenerGroups = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const rule of resolvedRules) {
|
|
1335
|
+
const { port, protocol } = parsePortProtocol(rule.listen);
|
|
1336
|
+
if (!listenerGroups.has(port)) {
|
|
1337
|
+
listenerGroups.set(port, { protocol, rules: [] });
|
|
1338
|
+
}
|
|
1339
|
+
listenerGroups.get(port).rules.push(rule);
|
|
1340
|
+
}
|
|
1341
|
+
const certificate = lb.domain ? acm2.Certificate.fromCertificateArn(this, "Certificate", lb.domain.certificateArn) : void 0;
|
|
1342
|
+
let primaryListener;
|
|
1343
|
+
for (const [listenPort, group] of listenerGroups) {
|
|
1344
|
+
const isHttps = group.protocol === "https";
|
|
1345
|
+
const defaultRedirect = group.rules.find((r) => r.redirect && !r.conditions);
|
|
1346
|
+
const defaultForward = group.rules.find((r) => !r.redirect && !r.conditions);
|
|
1347
|
+
const conditionalForwards = group.rules.filter((r) => !r.redirect && !!r.conditions);
|
|
1348
|
+
let listener;
|
|
1349
|
+
if (defaultRedirect) {
|
|
1350
|
+
const target = parsePortProtocol(defaultRedirect.redirect);
|
|
1351
|
+
let listenerProps = {
|
|
1352
|
+
port: listenPort,
|
|
1353
|
+
defaultAction: elbv2.ListenerAction.redirect({
|
|
1354
|
+
protocol: target.protocol.toUpperCase(),
|
|
1355
|
+
port: String(target.port),
|
|
1356
|
+
permanent: true
|
|
1357
|
+
})
|
|
1358
|
+
};
|
|
1359
|
+
if (props.transform?.listener) {
|
|
1360
|
+
listenerProps = props.transform.listener(listenerProps);
|
|
1361
|
+
}
|
|
1362
|
+
listener = this.alb.addListener(`Listener${listenPort}`, listenerProps);
|
|
1363
|
+
} else if (defaultForward) {
|
|
1364
|
+
const forwardPort = resolveForwardPort(defaultForward, props.containers);
|
|
1365
|
+
const tgKey = `${defaultForward.container}:${forwardPort}`;
|
|
1366
|
+
let listenerProps = {
|
|
1367
|
+
port: listenPort,
|
|
1368
|
+
...isHttps && certificate ? { certificates: [certificate] } : {},
|
|
1369
|
+
defaultTargetGroups: [tgMap[tgKey]]
|
|
1370
|
+
};
|
|
1371
|
+
if (props.transform?.listener) {
|
|
1372
|
+
listenerProps = props.transform.listener(listenerProps);
|
|
1373
|
+
}
|
|
1374
|
+
listener = this.alb.addListener(`Listener${listenPort}`, listenerProps);
|
|
1375
|
+
} else {
|
|
1376
|
+
throw new Error(`Listener on port ${listenPort} has no default rule (a rule without conditions is required)`);
|
|
1377
|
+
}
|
|
1378
|
+
if (!primaryListener && !defaultRedirect) {
|
|
1379
|
+
primaryListener = listener;
|
|
1380
|
+
}
|
|
1381
|
+
let autoPriority = 1;
|
|
1382
|
+
for (const rule of conditionalForwards) {
|
|
1383
|
+
const forwardPort = resolveForwardPort(rule, props.containers);
|
|
1384
|
+
const tgKey = `${rule.container}:${forwardPort}`;
|
|
1385
|
+
const priority = autoPriority++;
|
|
1386
|
+
const conditions = [];
|
|
1387
|
+
if (rule.conditions.path) {
|
|
1388
|
+
conditions.push(elbv2.ListenerCondition.pathPatterns([rule.conditions.path]));
|
|
1389
|
+
}
|
|
1390
|
+
if (rule.conditions.header) {
|
|
1391
|
+
conditions.push(elbv2.ListenerCondition.httpHeader(rule.conditions.header.name, rule.conditions.header.values));
|
|
1392
|
+
}
|
|
1393
|
+
if (rule.conditions.query) {
|
|
1394
|
+
conditions.push(elbv2.ListenerCondition.queryStrings(rule.conditions.query));
|
|
1395
|
+
}
|
|
1396
|
+
new elbv2.ApplicationListenerRule(this, `Listener${listenPort}Rule${priority}`, {
|
|
1397
|
+
listener,
|
|
1398
|
+
priority,
|
|
1399
|
+
conditions,
|
|
1400
|
+
targetGroups: [tgMap[tgKey]]
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
albSg.addIngressRule(ec22.Peer.anyIpv4(), ec22.Port.tcp(listenPort), `Port ${listenPort}`);
|
|
1404
|
+
}
|
|
1405
|
+
this.listener = primaryListener;
|
|
1406
|
+
const forwardPorts = /* @__PURE__ */ new Set();
|
|
1407
|
+
for (const rule of resolvedRules) {
|
|
1408
|
+
if (rule.redirect) continue;
|
|
1409
|
+
forwardPorts.add(resolveForwardPort(rule, props.containers));
|
|
1410
|
+
}
|
|
1411
|
+
for (const port of forwardPorts) {
|
|
1412
|
+
this.securityGroup.addIngressRule(albSg, ec22.Port.tcp(port), `From ALB on port ${port}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const capacityProviderStrategies = resolveCapacity(props.capacity);
|
|
1416
|
+
const desiredCount = props.scaling?.min ?? 1;
|
|
1417
|
+
let serviceProps = {
|
|
1418
|
+
cluster: props.cluster,
|
|
1419
|
+
taskDefinition: this.taskDefinition,
|
|
1420
|
+
desiredCount,
|
|
1421
|
+
securityGroups: [this.securityGroup],
|
|
1422
|
+
vpcSubnets: { subnetType: ec22.SubnetType.PRIVATE_WITH_EGRESS },
|
|
1423
|
+
assignPublicIp: false,
|
|
1424
|
+
circuitBreaker: { rollback: true },
|
|
1425
|
+
...capacityProviderStrategies ? { capacityProviderStrategies } : {}
|
|
1426
|
+
};
|
|
1427
|
+
if (props.transform?.service) {
|
|
1428
|
+
serviceProps = props.transform.service(serviceProps);
|
|
1429
|
+
}
|
|
1430
|
+
this.service = new ecs3.FargateService(this, "Service", serviceProps);
|
|
1431
|
+
if (this.targetGroups) {
|
|
1432
|
+
for (const tg of Object.values(this.targetGroups)) {
|
|
1433
|
+
this.service.attachToApplicationTargetGroup(tg);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (props.scaling) {
|
|
1437
|
+
let autoScalingProps = {
|
|
1438
|
+
minCapacity: props.scaling.min ?? 1,
|
|
1439
|
+
maxCapacity: props.scaling.max ?? 4
|
|
1440
|
+
};
|
|
1441
|
+
if (props.transform?.autoScalingTarget) {
|
|
1442
|
+
autoScalingProps = props.transform.autoScalingTarget(autoScalingProps);
|
|
1443
|
+
}
|
|
1444
|
+
const scaling = this.service.autoScaleTaskCount(autoScalingProps);
|
|
1445
|
+
scaling.scaleOnCpuUtilization("CpuScaling", {
|
|
1446
|
+
targetUtilizationPercent: props.scaling.cpuTarget ?? 70,
|
|
1447
|
+
scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
|
|
1448
|
+
scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
|
|
1449
|
+
});
|
|
1450
|
+
if (props.scaling.memoryTarget) {
|
|
1451
|
+
scaling.scaleOnMemoryUtilization("MemoryScaling", {
|
|
1452
|
+
targetUtilizationPercent: props.scaling.memoryTarget,
|
|
1453
|
+
scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
|
|
1454
|
+
scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
if (props.scaling.requestsPerTarget && this.targetGroups) {
|
|
1458
|
+
const targetGroup = Object.values(this.targetGroups)[0];
|
|
1459
|
+
scaling.scaleOnRequestCount("RequestScaling", {
|
|
1460
|
+
requestsPerTarget: props.scaling.requestsPerTarget,
|
|
1461
|
+
targetGroup,
|
|
1462
|
+
scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
|
|
1463
|
+
scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
function resolveContainerName(c, containers) {
|
|
1470
|
+
return c.name ?? (containers.length === 1 ? "Container" : void 0);
|
|
1471
|
+
}
|
|
1472
|
+
function parsePortProtocol(value) {
|
|
1473
|
+
const parts = value.split("/");
|
|
1474
|
+
return { port: Number(parts[0]), protocol: parts[1] ?? "http" };
|
|
1475
|
+
}
|
|
1476
|
+
function resolveDefaultContainerName(containers) {
|
|
1477
|
+
if (containers.length === 1) return resolveContainerName(containers[0], containers);
|
|
1478
|
+
throw new Error("container is required in routing rules when multiple containers are defined");
|
|
1479
|
+
}
|
|
1480
|
+
function resolveForwardPort(rule, containers) {
|
|
1481
|
+
if (rule.forward !== void 0) return rule.forward;
|
|
1482
|
+
const container = containers.find((c) => {
|
|
1483
|
+
return resolveContainerName(c, containers) === rule.container;
|
|
1484
|
+
});
|
|
1485
|
+
if (!container?.port) {
|
|
1486
|
+
throw new Error(`Container '${rule.container}' has no port and no forward specified in rule`);
|
|
1487
|
+
}
|
|
1488
|
+
return container.port;
|
|
1489
|
+
}
|
|
1490
|
+
function autoRules(containers, hasDomain) {
|
|
1491
|
+
const withPorts = containers.filter((c) => c.port);
|
|
1492
|
+
if (withPorts.length === 0) {
|
|
1493
|
+
throw new Error("No container with a port found for load balancer");
|
|
1494
|
+
}
|
|
1495
|
+
if (withPorts.length > 1) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
"Multiple containers have ports but no routing rules are defined. Add loadBalancer.rules to specify routing."
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
const name = resolveContainerName(withPorts[0], containers);
|
|
1501
|
+
if (hasDomain) {
|
|
1502
|
+
return [
|
|
1503
|
+
{ listen: "443/https", container: name },
|
|
1504
|
+
{ listen: "80/http", redirect: "443/https" }
|
|
1505
|
+
];
|
|
1506
|
+
}
|
|
1507
|
+
return [{ listen: "80/http", container: name }];
|
|
1508
|
+
}
|
|
1509
|
+
function resolveImage(scope, image, platform, id) {
|
|
1510
|
+
if (image === void 0) {
|
|
1511
|
+
return ecs3.ContainerImage.fromAsset(".", { platform });
|
|
1512
|
+
}
|
|
1513
|
+
if (typeof image === "string") {
|
|
1514
|
+
return ecs3.ContainerImage.fromRegistry(image);
|
|
1515
|
+
}
|
|
1516
|
+
if (image instanceof ecrAssets.DockerImageAsset) {
|
|
1517
|
+
return ecs3.ContainerImage.fromDockerImageAsset(image);
|
|
1518
|
+
}
|
|
1519
|
+
return ecs3.ContainerImage.fromAsset(image.context ?? ".", {
|
|
1520
|
+
platform,
|
|
1521
|
+
file: image.dockerfile,
|
|
1522
|
+
buildArgs: image.args,
|
|
1523
|
+
target: image.target,
|
|
1524
|
+
exclude: image.exclude
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function resolveCapacity(capacity) {
|
|
1528
|
+
if (!capacity) return void 0;
|
|
1529
|
+
if (capacity === "spot") {
|
|
1530
|
+
return [{ capacityProvider: "FARGATE_SPOT", weight: 1 }];
|
|
1531
|
+
}
|
|
1532
|
+
const strategies = [];
|
|
1533
|
+
if (capacity.fargate) {
|
|
1534
|
+
strategies.push({
|
|
1535
|
+
capacityProvider: "FARGATE",
|
|
1536
|
+
weight: capacity.fargate.weight,
|
|
1537
|
+
base: capacity.fargate.base
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
if (capacity.spot) {
|
|
1541
|
+
strategies.push({
|
|
1542
|
+
capacityProvider: "FARGATE_SPOT",
|
|
1543
|
+
weight: capacity.spot.weight,
|
|
1544
|
+
base: capacity.spot.base
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
return strategies.length > 0 ? strategies : void 0;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/helpers/ssr-site-ecs-service.ts
|
|
1551
|
+
function createSsrSiteEcsService(scope, props) {
|
|
1552
|
+
const assetBehaviors = props.assets?.behaviors ?? [];
|
|
1553
|
+
let bucketProps = {
|
|
1554
|
+
blockPublicAccess: s35.BlockPublicAccess.BLOCK_ALL,
|
|
1555
|
+
removalPolicy: RemovalPolicy4.RETAIN
|
|
1556
|
+
};
|
|
1557
|
+
if (props.transform?.bucket) {
|
|
1558
|
+
bucketProps = props.transform.bucket(bucketProps);
|
|
1559
|
+
}
|
|
1560
|
+
const bucket = new s35.Bucket(scope, "Bucket", bucketProps);
|
|
1561
|
+
const ecsService = new EcsFargateService(scope, "Ecs", {
|
|
1562
|
+
cluster: props.cluster,
|
|
1563
|
+
architecture: props.architecture,
|
|
1564
|
+
cpu: props.cpu,
|
|
1565
|
+
memoryLimitMiB: props.memoryLimitMiB,
|
|
1566
|
+
capacity: props.capacity,
|
|
1567
|
+
containers: [
|
|
1568
|
+
{
|
|
1569
|
+
image: props.image,
|
|
1570
|
+
port: props.port ?? 3e3,
|
|
1571
|
+
command: props.command,
|
|
1572
|
+
entrypoint: props.entrypoint,
|
|
1573
|
+
environment: props.environment,
|
|
1574
|
+
ssm: props.ssm,
|
|
1575
|
+
secrets: props.secrets
|
|
1576
|
+
}
|
|
1577
|
+
],
|
|
1578
|
+
loadBalancer: {
|
|
1579
|
+
public: true,
|
|
1580
|
+
health: props.healthCheck
|
|
1581
|
+
},
|
|
1582
|
+
scaling: props.scaling,
|
|
1583
|
+
permissions: props.permissions,
|
|
1584
|
+
transform: {
|
|
1585
|
+
executionRole: props.transform?.executionRole,
|
|
1586
|
+
taskRole: props.transform?.taskRole,
|
|
1587
|
+
taskDefinition: props.transform?.taskDefinition,
|
|
1588
|
+
container: props.transform?.container,
|
|
1589
|
+
service: props.transform?.service,
|
|
1590
|
+
autoScalingTarget: props.transform?.autoScalingTarget,
|
|
1591
|
+
alb: props.transform?.alb,
|
|
1592
|
+
albSecurityGroup: props.transform?.albSecurityGroup,
|
|
1593
|
+
listener: props.transform?.listener,
|
|
1594
|
+
targetGroup: props.transform?.targetGroup,
|
|
1595
|
+
logGroup: props.transform?.logGroup
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
const alb = ecsService.alb;
|
|
1599
|
+
const cfFunction = props.edge?.viewerRequest ?? createViewerRequestFunction(scope, "ViewerRequestFn");
|
|
1600
|
+
const defaultFunctionAssociations = [
|
|
1601
|
+
{ function: cfFunction, eventType: cloudfront4.FunctionEventType.VIEWER_REQUEST }
|
|
1602
|
+
];
|
|
1603
|
+
if (props.edge?.viewerResponse) {
|
|
1604
|
+
defaultFunctionAssociations.push({
|
|
1605
|
+
function: props.edge.viewerResponse,
|
|
1606
|
+
eventType: cloudfront4.FunctionEventType.VIEWER_RESPONSE
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const { certificate, domainNames } = resolveCertificate(scope, props.domain);
|
|
1610
|
+
const s3Origin = origins3.S3BucketOrigin.withOriginAccessControl(bucket);
|
|
1611
|
+
const albOrigin = new origins3.HttpOrigin(alb.loadBalancerDnsName, {
|
|
1612
|
+
protocolPolicy: cloudfront4.OriginProtocolPolicy.HTTP_ONLY
|
|
1613
|
+
});
|
|
1614
|
+
const additionalBehaviors = resolveAdditionalBehaviors(s3Origin, assetBehaviors);
|
|
1615
|
+
let distributionProps = {
|
|
1616
|
+
defaultBehavior: {
|
|
1617
|
+
origin: albOrigin,
|
|
1618
|
+
viewerProtocolPolicy: cloudfront4.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
1619
|
+
cachePolicy: cloudfront4.CachePolicy.CACHING_DISABLED,
|
|
1620
|
+
originRequestPolicy: cloudfront4.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
1621
|
+
allowedMethods: cloudfront4.AllowedMethods.ALLOW_ALL,
|
|
1622
|
+
functionAssociations: defaultFunctionAssociations
|
|
1623
|
+
},
|
|
1624
|
+
additionalBehaviors,
|
|
1625
|
+
domainNames,
|
|
1626
|
+
certificate
|
|
1627
|
+
};
|
|
1628
|
+
if (props.transform?.distribution) {
|
|
1629
|
+
distributionProps = props.transform.distribution(distributionProps);
|
|
1630
|
+
}
|
|
1631
|
+
const distribution = new cloudfront4.Distribution(scope, "Distribution", distributionProps);
|
|
1632
|
+
bucket.grantReadWrite(ecsService.taskDefinition.taskRole);
|
|
1633
|
+
ecsService.taskDefinition.taskRole.addToPrincipalPolicy(
|
|
1634
|
+
new iam6.PolicyStatement({
|
|
1635
|
+
actions: ["cloudfront:CreateInvalidation"],
|
|
1636
|
+
resources: [`arn:aws:cloudfront::*:distribution/${distribution.distributionId}`]
|
|
1637
|
+
})
|
|
1638
|
+
);
|
|
1639
|
+
ecsService.containers["Container"].addEnvironment("CLOUDFRONT_DISTRIBUTION_ID", distribution.distributionId);
|
|
1640
|
+
ecsService.containers["Container"].addEnvironment("S3_BUCKET_NAME", bucket.bucketName);
|
|
1641
|
+
if (props.assets?.path) {
|
|
1642
|
+
const fileOptions = props.assets.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }];
|
|
1643
|
+
resolveAssetDeployment(scope, "", {
|
|
1644
|
+
path: props.assets.path,
|
|
1645
|
+
bucket,
|
|
1646
|
+
prefix: props.assets.prefix,
|
|
1647
|
+
fileOptions,
|
|
1648
|
+
distribution,
|
|
1649
|
+
invalidation: props.invalidation
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
return { bucket, distribution, ecsService };
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/react-router-ssr-site-ecs-fargate-service.ts
|
|
1656
|
+
var ReactRouterSsrSiteEcsFargateService = class extends Construct12 {
|
|
1657
|
+
bucket;
|
|
1658
|
+
distribution;
|
|
1659
|
+
ecsService;
|
|
1660
|
+
constructor(scope, id, props) {
|
|
1661
|
+
super(scope, id);
|
|
1662
|
+
const buildDir = props.buildDirectory ?? "build";
|
|
1663
|
+
const appRoot = !props.path || props.path === "." ? "." : props.path;
|
|
1664
|
+
const basePath = appRoot === "." ? buildDir : `${appRoot}/${buildDir}`;
|
|
1665
|
+
const port = props.port ?? 3e3;
|
|
1666
|
+
let image = props.image;
|
|
1667
|
+
if (!image) {
|
|
1668
|
+
image = resolveReactRouterDockerContext({
|
|
1669
|
+
appRoot,
|
|
1670
|
+
buildDir,
|
|
1671
|
+
basePath,
|
|
1672
|
+
port,
|
|
1673
|
+
className: "ReactRouterSsrSiteEcsFargateService"
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
const merged = {
|
|
1677
|
+
cluster: props.cluster,
|
|
1678
|
+
image,
|
|
1679
|
+
port,
|
|
1680
|
+
command: props.command,
|
|
1681
|
+
entrypoint: props.entrypoint,
|
|
1682
|
+
environment: props.environment,
|
|
1683
|
+
ssm: props.ssm,
|
|
1684
|
+
secrets: props.secrets,
|
|
1685
|
+
architecture: props.architecture,
|
|
1686
|
+
cpu: props.cpu,
|
|
1687
|
+
memoryLimitMiB: props.memoryLimitMiB,
|
|
1688
|
+
capacity: props.capacity,
|
|
1689
|
+
healthCheck: props.healthCheck,
|
|
1690
|
+
scaling: props.scaling,
|
|
1691
|
+
permissions: props.permissions,
|
|
1692
|
+
domain: props.domain,
|
|
1693
|
+
edge: props.edge,
|
|
1694
|
+
invalidation: props.invalidation,
|
|
1695
|
+
transform: props.transform,
|
|
1696
|
+
assets: {
|
|
1697
|
+
...props.assets,
|
|
1698
|
+
path: props.assets?.path ?? `${basePath}/client`,
|
|
1699
|
+
behaviors: props.assets?.behaviors ?? [{ pathPattern: "/assets/*", cachePolicy: "CACHING_OPTIMIZED" }],
|
|
1700
|
+
fileOptions: props.assets?.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }]
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const resources = createSsrSiteEcsService(this, merged);
|
|
1704
|
+
this.bucket = resources.bucket;
|
|
1705
|
+
this.distribution = resources.distribution;
|
|
1706
|
+
this.ecsService = resources.ecsService;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
// src/react-router-ecr-image.ts
|
|
1711
|
+
import { Construct as Construct13 } from "constructs";
|
|
1712
|
+
import * as ecrAssets2 from "aws-cdk-lib/aws-ecr-assets";
|
|
1713
|
+
var ReactRouterEcrImage = class extends Construct13 {
|
|
1714
|
+
asset;
|
|
1715
|
+
constructor(scope, id, props = {}) {
|
|
1716
|
+
super(scope, id);
|
|
1717
|
+
const buildDir = props.buildDirectory ?? "build";
|
|
1718
|
+
const appRoot = !props.path || props.path === "." ? "." : props.path;
|
|
1719
|
+
const basePath = appRoot === "." ? buildDir : `${appRoot}/${buildDir}`;
|
|
1720
|
+
const port = props.port ?? 3e3;
|
|
1721
|
+
const arch = props.architecture ?? "X86_64";
|
|
1722
|
+
const platform = props.platform ?? (arch === "ARM64" ? ecrAssets2.Platform.LINUX_ARM64 : ecrAssets2.Platform.LINUX_AMD64);
|
|
1723
|
+
let context;
|
|
1724
|
+
let dockerfile;
|
|
1725
|
+
if (props.image) {
|
|
1726
|
+
context = props.image.context ?? appRoot;
|
|
1727
|
+
dockerfile = props.image.dockerfile;
|
|
1728
|
+
} else {
|
|
1729
|
+
const resolved = resolveReactRouterDockerContext({
|
|
1730
|
+
appRoot,
|
|
1731
|
+
buildDir,
|
|
1732
|
+
basePath,
|
|
1733
|
+
port,
|
|
1734
|
+
className: "ReactRouterEcrImage"
|
|
1735
|
+
});
|
|
1736
|
+
context = resolved.context;
|
|
1737
|
+
dockerfile = resolved.dockerfile;
|
|
1738
|
+
}
|
|
1739
|
+
this.asset = new ecrAssets2.DockerImageAsset(this, "Image", {
|
|
1740
|
+
directory: context,
|
|
1741
|
+
file: dockerfile,
|
|
1742
|
+
platform,
|
|
1743
|
+
buildArgs: props.image?.args,
|
|
1744
|
+
target: props.image?.target,
|
|
1745
|
+
exclude: props.image?.exclude
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
// src/mysql-database.ts
|
|
1751
|
+
import { Construct as Construct14 } from "constructs";
|
|
1752
|
+
import { Duration as Duration5, RemovalPolicy as RemovalPolicy5 } from "aws-cdk-lib";
|
|
1753
|
+
import * as ec23 from "aws-cdk-lib/aws-ec2";
|
|
1754
|
+
import * as rds from "aws-cdk-lib/aws-rds";
|
|
1755
|
+
var MySqlDatabase = class extends Construct14 {
|
|
1756
|
+
instance;
|
|
1757
|
+
securityGroup;
|
|
1758
|
+
secret;
|
|
1759
|
+
connections;
|
|
1760
|
+
rdsProxy;
|
|
1761
|
+
get host() {
|
|
1762
|
+
return this.instance.dbInstanceEndpointAddress;
|
|
1763
|
+
}
|
|
1764
|
+
get port() {
|
|
1765
|
+
return this.instance.dbInstanceEndpointPort;
|
|
1766
|
+
}
|
|
1767
|
+
get proxyEndpoint() {
|
|
1768
|
+
return this.rdsProxy?.endpoint;
|
|
1769
|
+
}
|
|
1770
|
+
constructor(scope, id, props) {
|
|
1771
|
+
super(scope, id);
|
|
1772
|
+
const username = props.username ?? "root";
|
|
1773
|
+
let instanceProps = {
|
|
1774
|
+
engine: rds.DatabaseInstanceEngine.mysql({
|
|
1775
|
+
version: props.version ?? rds.MysqlEngineVersion.VER_8_0
|
|
1776
|
+
}),
|
|
1777
|
+
instanceType: props.instance ?? ec23.InstanceType.of(ec23.InstanceClass.T3, ec23.InstanceSize.MICRO),
|
|
1778
|
+
vpc: props.vpc,
|
|
1779
|
+
vpcSubnets: { subnetType: resolvePrivateSubnetType(props.vpc) },
|
|
1780
|
+
databaseName: props.database,
|
|
1781
|
+
credentials: rds.Credentials.fromGeneratedSecret(username),
|
|
1782
|
+
maxAllocatedStorage: props.storage ? Math.max(props.storage, 5e3) : 5e3,
|
|
1783
|
+
allocatedStorage: props.storage ?? 20,
|
|
1784
|
+
multiAz: props.multiAz ?? false,
|
|
1785
|
+
backupRetention: Duration5.days(props.backupRetentionDays ?? 7),
|
|
1786
|
+
deletionProtection: props.deletionProtection ?? true,
|
|
1787
|
+
removalPolicy: RemovalPolicy5.SNAPSHOT,
|
|
1788
|
+
storageEncrypted: true,
|
|
1789
|
+
enablePerformanceInsights: props.performanceInsights ?? false,
|
|
1790
|
+
port: 3306
|
|
1791
|
+
};
|
|
1792
|
+
if (props.transform?.instance) {
|
|
1793
|
+
instanceProps = props.transform.instance(instanceProps);
|
|
1794
|
+
}
|
|
1795
|
+
this.instance = new rds.DatabaseInstance(this, "Instance", instanceProps);
|
|
1796
|
+
this.securityGroup = this.instance.connections.securityGroups[0];
|
|
1797
|
+
this.secret = this.instance.secret;
|
|
1798
|
+
this.connections = this.instance.connections;
|
|
1799
|
+
if (props.proxy) {
|
|
1800
|
+
let proxyProps = {
|
|
1801
|
+
proxyTarget: rds.ProxyTarget.fromInstance(this.instance),
|
|
1802
|
+
secrets: [this.secret],
|
|
1803
|
+
vpc: props.vpc,
|
|
1804
|
+
securityGroups: [this.securityGroup],
|
|
1805
|
+
requireTLS: false
|
|
1806
|
+
};
|
|
1807
|
+
if (props.transform?.proxy) {
|
|
1808
|
+
proxyProps = props.transform.proxy(proxyProps);
|
|
1809
|
+
}
|
|
1810
|
+
this.rdsProxy = new rds.DatabaseProxy(this, "Proxy", proxyProps);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
function resolvePrivateSubnetType(vpc) {
|
|
1815
|
+
if (vpc instanceof ec23.Vpc && vpc.isolatedSubnets.length > 0) return ec23.SubnetType.PRIVATE_ISOLATED;
|
|
1816
|
+
return ec23.SubnetType.PRIVATE_WITH_EGRESS;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/aurora-database.ts
|
|
1820
|
+
import { Construct as Construct15 } from "constructs";
|
|
1821
|
+
import { Duration as Duration6, RemovalPolicy as RemovalPolicy6 } from "aws-cdk-lib";
|
|
1822
|
+
import * as ec24 from "aws-cdk-lib/aws-ec2";
|
|
1823
|
+
import * as rds2 from "aws-cdk-lib/aws-rds";
|
|
1824
|
+
var AuroraDatabase = class extends Construct15 {
|
|
1825
|
+
cluster;
|
|
1826
|
+
securityGroup;
|
|
1827
|
+
secret;
|
|
1828
|
+
connections;
|
|
1829
|
+
rdsProxy;
|
|
1830
|
+
get host() {
|
|
1831
|
+
return this.cluster.clusterEndpoint.hostname;
|
|
1832
|
+
}
|
|
1833
|
+
get port() {
|
|
1834
|
+
return this.cluster.clusterEndpoint.port.toString();
|
|
1835
|
+
}
|
|
1836
|
+
get readerHost() {
|
|
1837
|
+
return this.cluster.clusterReadEndpoint.hostname;
|
|
1838
|
+
}
|
|
1839
|
+
get proxyEndpoint() {
|
|
1840
|
+
return this.rdsProxy?.endpoint;
|
|
1841
|
+
}
|
|
1842
|
+
constructor(scope, id, props) {
|
|
1843
|
+
super(scope, id);
|
|
1844
|
+
const engine = resolveEngine(props.engine, props.version);
|
|
1845
|
+
const username = props.username ?? (props.engine === "postgres" ? "postgres" : "root");
|
|
1846
|
+
const readers = Array.from(
|
|
1847
|
+
{ length: props.readers ?? 0 },
|
|
1848
|
+
(_, i) => rds2.ClusterInstance.serverlessV2(`Reader${i + 1}`, { scaleWithWriter: true })
|
|
1849
|
+
);
|
|
1850
|
+
let clusterProps = {
|
|
1851
|
+
engine,
|
|
1852
|
+
port: props.port,
|
|
1853
|
+
writer: rds2.ClusterInstance.serverlessV2("Writer"),
|
|
1854
|
+
readers,
|
|
1855
|
+
serverlessV2MinCapacity: props.scaling?.min ?? 0,
|
|
1856
|
+
serverlessV2MaxCapacity: props.scaling?.max ?? 4,
|
|
1857
|
+
serverlessV2AutoPauseDuration: props.scaling?.pauseAfter ? parseDuration(props.scaling.pauseAfter) : void 0,
|
|
1858
|
+
vpc: props.vpc,
|
|
1859
|
+
vpcSubnets: { subnetType: resolvePrivateSubnetType2(props.vpc) },
|
|
1860
|
+
defaultDatabaseName: props.database,
|
|
1861
|
+
credentials: rds2.Credentials.fromGeneratedSecret(username),
|
|
1862
|
+
storageEncrypted: true,
|
|
1863
|
+
deletionProtection: props.deletionProtection ?? true,
|
|
1864
|
+
removalPolicy: RemovalPolicy6.SNAPSHOT,
|
|
1865
|
+
backup: { retention: Duration6.days(props.backupRetentionDays ?? 7) },
|
|
1866
|
+
enableDataApi: props.dataApi ?? false,
|
|
1867
|
+
enablePerformanceInsights: props.performanceInsights ?? false
|
|
1868
|
+
};
|
|
1869
|
+
if (props.transform?.cluster) {
|
|
1870
|
+
clusterProps = props.transform.cluster(clusterProps);
|
|
1871
|
+
}
|
|
1872
|
+
this.cluster = new rds2.DatabaseCluster(this, "Cluster", clusterProps);
|
|
1873
|
+
if (!this.cluster.secret) {
|
|
1874
|
+
throw new Error("AuroraDatabase: cluster secret is missing \u2014 ensure credentials are not overridden via transform");
|
|
1875
|
+
}
|
|
1876
|
+
this.securityGroup = this.cluster.connections.securityGroups[0];
|
|
1877
|
+
this.secret = this.cluster.secret;
|
|
1878
|
+
this.connections = this.cluster.connections;
|
|
1879
|
+
if (props.proxy) {
|
|
1880
|
+
let proxyProps = {
|
|
1881
|
+
proxyTarget: rds2.ProxyTarget.fromCluster(this.cluster),
|
|
1882
|
+
secrets: [this.secret],
|
|
1883
|
+
vpc: props.vpc,
|
|
1884
|
+
securityGroups: [this.securityGroup],
|
|
1885
|
+
requireTLS: false
|
|
1886
|
+
};
|
|
1887
|
+
if (props.transform?.proxy) {
|
|
1888
|
+
proxyProps = props.transform.proxy(proxyProps);
|
|
1889
|
+
}
|
|
1890
|
+
this.rdsProxy = new rds2.DatabaseProxy(this, "Proxy", proxyProps);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
function parseDuration(value) {
|
|
1895
|
+
const match = value.match(/^(\d+)\s+(second|seconds|minute|minutes|hour|hours)$/);
|
|
1896
|
+
if (!match) throw new Error(`Invalid duration: ${value}`);
|
|
1897
|
+
const amount = Number(match[1]);
|
|
1898
|
+
const unit = match[2];
|
|
1899
|
+
if (unit.startsWith("hour")) return Duration6.hours(amount);
|
|
1900
|
+
if (unit.startsWith("minute")) return Duration6.minutes(amount);
|
|
1901
|
+
return Duration6.seconds(amount);
|
|
1902
|
+
}
|
|
1903
|
+
function resolvePrivateSubnetType2(vpc) {
|
|
1904
|
+
if (vpc instanceof ec24.Vpc && vpc.isolatedSubnets.length > 0) return ec24.SubnetType.PRIVATE_ISOLATED;
|
|
1905
|
+
return ec24.SubnetType.PRIVATE_WITH_EGRESS;
|
|
1906
|
+
}
|
|
1907
|
+
function resolveEngine(engine, version) {
|
|
1908
|
+
if (engine === "postgres") {
|
|
1909
|
+
return rds2.DatabaseClusterEngine.auroraPostgres({
|
|
1910
|
+
version
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
return rds2.DatabaseClusterEngine.auroraMysql({
|
|
1914
|
+
version
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/redis.ts
|
|
1919
|
+
import { Construct as Construct16 } from "constructs";
|
|
1920
|
+
import * as ec25 from "aws-cdk-lib/aws-ec2";
|
|
1921
|
+
import * as elasticache from "aws-cdk-lib/aws-elasticache";
|
|
1922
|
+
import * as secretsmanager2 from "aws-cdk-lib/aws-secretsmanager";
|
|
1923
|
+
var Redis = class extends Construct16 {
|
|
1924
|
+
replicationGroup;
|
|
1925
|
+
securityGroup;
|
|
1926
|
+
subnetGroup;
|
|
1927
|
+
parameterGroup;
|
|
1928
|
+
secret;
|
|
1929
|
+
get host() {
|
|
1930
|
+
return this.replicationGroup.attrPrimaryEndPointAddress;
|
|
1931
|
+
}
|
|
1932
|
+
get port() {
|
|
1933
|
+
return this.replicationGroup.attrPrimaryEndPointPort;
|
|
1934
|
+
}
|
|
1935
|
+
constructor(scope, id, props) {
|
|
1936
|
+
super(scope, id);
|
|
1937
|
+
const engine = props.engine ?? "redis";
|
|
1938
|
+
const version = resolveVersion(engine, props.version);
|
|
1939
|
+
const replicas = props.replicas ?? 0;
|
|
1940
|
+
this.securityGroup = new ec25.SecurityGroup(this, "SecurityGroup", {
|
|
1941
|
+
vpc: props.vpc,
|
|
1942
|
+
description: `Security group for ${id} ElastiCache`
|
|
1943
|
+
});
|
|
1944
|
+
this.securityGroup.addIngressRule(ec25.Peer.ipv4(props.vpc.vpcCidrBlock), ec25.Port.tcp(6379), "Allow Redis access from VPC");
|
|
1945
|
+
const subnetType = resolvePrivateSubnetType3(props.vpc);
|
|
1946
|
+
const subnets = props.vpc.selectSubnets({ subnetType });
|
|
1947
|
+
let subnetGroupProps = {
|
|
1948
|
+
description: `Subnet group for ${id}`,
|
|
1949
|
+
subnetIds: subnets.subnetIds
|
|
1950
|
+
};
|
|
1951
|
+
if (props.transform?.subnetGroup) {
|
|
1952
|
+
subnetGroupProps = props.transform.subnetGroup(subnetGroupProps);
|
|
1953
|
+
}
|
|
1954
|
+
this.subnetGroup = new elasticache.CfnSubnetGroup(this, "SubnetGroup", subnetGroupProps);
|
|
1955
|
+
if (props.parameters) {
|
|
1956
|
+
let parameterGroupProps = {
|
|
1957
|
+
cacheParameterGroupFamily: resolveParameterGroupFamily(engine, version),
|
|
1958
|
+
description: `Parameter group for ${id}`,
|
|
1959
|
+
properties: props.parameters
|
|
1960
|
+
};
|
|
1961
|
+
if (props.transform?.parameterGroup) {
|
|
1962
|
+
parameterGroupProps = props.transform.parameterGroup(parameterGroupProps);
|
|
1963
|
+
}
|
|
1964
|
+
this.parameterGroup = new elasticache.CfnParameterGroup(this, "ParameterGroup", parameterGroupProps);
|
|
1965
|
+
}
|
|
1966
|
+
this.secret = new secretsmanager2.Secret(this, "AuthToken", {
|
|
1967
|
+
description: `Auth token for ${id} ElastiCache`,
|
|
1968
|
+
generateSecretString: {
|
|
1969
|
+
excludePunctuation: true,
|
|
1970
|
+
passwordLength: 64
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
let replicationGroupProps = {
|
|
1974
|
+
replicationGroupDescription: `${id} replication group`,
|
|
1975
|
+
engine,
|
|
1976
|
+
engineVersion: version,
|
|
1977
|
+
cacheNodeType: props.instance ?? "cache.t4g.micro",
|
|
1978
|
+
numCacheClusters: replicas + 1,
|
|
1979
|
+
port: 6379,
|
|
1980
|
+
automaticFailoverEnabled: replicas >= 1,
|
|
1981
|
+
multiAzEnabled: false,
|
|
1982
|
+
cacheSubnetGroupName: this.subnetGroup.ref,
|
|
1983
|
+
cacheParameterGroupName: this.parameterGroup?.ref,
|
|
1984
|
+
securityGroupIds: [this.securityGroup.securityGroupId],
|
|
1985
|
+
transitEncryptionEnabled: true,
|
|
1986
|
+
atRestEncryptionEnabled: true,
|
|
1987
|
+
authToken: this.secret.secretValue.unsafeUnwrap()
|
|
1988
|
+
};
|
|
1989
|
+
if (props.transform?.replicationGroup) {
|
|
1990
|
+
replicationGroupProps = props.transform.replicationGroup(replicationGroupProps);
|
|
1991
|
+
}
|
|
1992
|
+
this.replicationGroup = new elasticache.CfnReplicationGroup(this, "ReplicationGroup", replicationGroupProps);
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
function resolvePrivateSubnetType3(vpc) {
|
|
1996
|
+
if (vpc instanceof ec25.Vpc && vpc.isolatedSubnets.length > 0) return ec25.SubnetType.PRIVATE_ISOLATED;
|
|
1997
|
+
return ec25.SubnetType.PRIVATE_WITH_EGRESS;
|
|
1998
|
+
}
|
|
1999
|
+
function resolveVersion(engine, version) {
|
|
2000
|
+
if (version) return version;
|
|
2001
|
+
return engine === "valkey" ? "7.2" : "7.1";
|
|
2002
|
+
}
|
|
2003
|
+
function resolveParameterGroupFamily(engine, version) {
|
|
2004
|
+
const major = version.split(".")[0];
|
|
2005
|
+
if (engine === "valkey") return `valkey${major}`;
|
|
2006
|
+
return `redis${major}.x`;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/email.ts
|
|
2010
|
+
import { Construct as Construct17 } from "constructs";
|
|
2011
|
+
import * as ses from "aws-cdk-lib/aws-ses";
|
|
2012
|
+
import * as route53 from "aws-cdk-lib/aws-route53";
|
|
2013
|
+
var Email = class extends Construct17 {
|
|
2014
|
+
identity;
|
|
2015
|
+
configurationSet;
|
|
2016
|
+
dmarcRecord;
|
|
2017
|
+
_sender;
|
|
2018
|
+
get sender() {
|
|
2019
|
+
return this._sender;
|
|
2020
|
+
}
|
|
2021
|
+
get configSetName() {
|
|
2022
|
+
return this.configurationSet.configurationSetName;
|
|
2023
|
+
}
|
|
2024
|
+
constructor(scope, id, props) {
|
|
2025
|
+
super(scope, id);
|
|
2026
|
+
this._sender = props.sender;
|
|
2027
|
+
const senderType = resolveSenderType(props.sender);
|
|
2028
|
+
if (senderType === "email" && props.dmarc !== void 0) {
|
|
2029
|
+
throw new Error("DMARC records can only be configured for domain senders, not email addresses");
|
|
2030
|
+
}
|
|
2031
|
+
let configurationSetProps = {
|
|
2032
|
+
reputationMetrics: true
|
|
2033
|
+
};
|
|
2034
|
+
if (props.transform?.configurationSet) {
|
|
2035
|
+
configurationSetProps = props.transform.configurationSet(configurationSetProps);
|
|
2036
|
+
}
|
|
2037
|
+
this.configurationSet = new ses.ConfigurationSet(this, "ConfigurationSet", configurationSetProps);
|
|
2038
|
+
const identity = resolveIdentity(senderType, props.sender, props.hostedZone);
|
|
2039
|
+
let identityProps = {
|
|
2040
|
+
identity,
|
|
2041
|
+
configurationSet: this.configurationSet
|
|
2042
|
+
};
|
|
2043
|
+
if (props.transform?.identity) {
|
|
2044
|
+
identityProps = props.transform.identity(identityProps);
|
|
2045
|
+
}
|
|
2046
|
+
this.identity = new ses.EmailIdentity(this, "Identity", identityProps);
|
|
2047
|
+
if (senderType === "domain" && props.hostedZone) {
|
|
2048
|
+
const dmarcValue = props.dmarc ?? "v=DMARC1; p=none;";
|
|
2049
|
+
this.dmarcRecord = new route53.TxtRecord(this, "DmarcRecord", {
|
|
2050
|
+
zone: props.hostedZone,
|
|
2051
|
+
recordName: `_dmarc.${props.sender}`,
|
|
2052
|
+
values: [dmarcValue]
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
if (props.events) {
|
|
2056
|
+
for (const [i, event] of props.events.entries()) {
|
|
2057
|
+
const types = event.types ? event.types.map(resolveEmailSendingEvent) : void 0;
|
|
2058
|
+
const destination = resolveEventDestination(event);
|
|
2059
|
+
this.configurationSet.addEventDestination(`EventDestination${i}`, {
|
|
2060
|
+
destination,
|
|
2061
|
+
enabled: event.enabled,
|
|
2062
|
+
events: types
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
function resolveSenderType(sender) {
|
|
2069
|
+
return sender.includes("@") ? "email" : "domain";
|
|
2070
|
+
}
|
|
2071
|
+
function resolveIdentity(senderType, sender, hostedZone) {
|
|
2072
|
+
if (senderType === "email") return ses.Identity.email(sender);
|
|
2073
|
+
if (hostedZone) return ses.Identity.publicHostedZone(hostedZone);
|
|
2074
|
+
return ses.Identity.domain(sender);
|
|
2075
|
+
}
|
|
2076
|
+
function resolveEventDestination(event) {
|
|
2077
|
+
if (event.snsTopic) return ses.EventDestination.snsTopic(event.snsTopic);
|
|
2078
|
+
if (event.cloudWatchDimensions) {
|
|
2079
|
+
return ses.EventDestination.cloudWatchDimensions(
|
|
2080
|
+
event.cloudWatchDimensions.map((d) => ({
|
|
2081
|
+
name: d.name,
|
|
2082
|
+
source: d.source,
|
|
2083
|
+
defaultValue: d.defaultValue
|
|
2084
|
+
}))
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
throw new Error('Event destination must specify either "snsTopic" or "cloudWatchDimensions"');
|
|
2088
|
+
}
|
|
2089
|
+
function resolveEmailSendingEvent(type) {
|
|
2090
|
+
switch (type) {
|
|
2091
|
+
case "send":
|
|
2092
|
+
return ses.EmailSendingEvent.SEND;
|
|
2093
|
+
case "reject":
|
|
2094
|
+
return ses.EmailSendingEvent.REJECT;
|
|
2095
|
+
case "bounce":
|
|
2096
|
+
return ses.EmailSendingEvent.BOUNCE;
|
|
2097
|
+
case "complaint":
|
|
2098
|
+
return ses.EmailSendingEvent.COMPLAINT;
|
|
2099
|
+
case "delivery":
|
|
2100
|
+
return ses.EmailSendingEvent.DELIVERY;
|
|
2101
|
+
case "open":
|
|
2102
|
+
return ses.EmailSendingEvent.OPEN;
|
|
2103
|
+
case "click":
|
|
2104
|
+
return ses.EmailSendingEvent.CLICK;
|
|
2105
|
+
case "rendering-failure":
|
|
2106
|
+
return ses.EmailSendingEvent.RENDERING_FAILURE;
|
|
2107
|
+
case "delivery-delay":
|
|
2108
|
+
return ses.EmailSendingEvent.DELIVERY_DELAY;
|
|
2109
|
+
case "subscription":
|
|
2110
|
+
return ses.EmailSendingEvent.SUBSCRIPTION;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/sns-topic.ts
|
|
2115
|
+
import { Construct as Construct18 } from "constructs";
|
|
2116
|
+
import * as sns from "aws-cdk-lib/aws-sns";
|
|
2117
|
+
import * as snsSubscriptions from "aws-cdk-lib/aws-sns-subscriptions";
|
|
2118
|
+
var SnsTopic = class extends Construct18 {
|
|
2119
|
+
topic;
|
|
2120
|
+
get arn() {
|
|
2121
|
+
return this.topic.topicArn;
|
|
2122
|
+
}
|
|
2123
|
+
get topicName() {
|
|
2124
|
+
return this.topic.topicName;
|
|
2125
|
+
}
|
|
2126
|
+
constructor(scope, id, props = {}) {
|
|
2127
|
+
super(scope, id);
|
|
2128
|
+
let topicProps = {
|
|
2129
|
+
displayName: props.displayName,
|
|
2130
|
+
fifo: props.fifo
|
|
2131
|
+
};
|
|
2132
|
+
if (props.transform?.topic) {
|
|
2133
|
+
topicProps = props.transform.topic(topicProps);
|
|
2134
|
+
}
|
|
2135
|
+
this.topic = new sns.Topic(this, "Topic", topicProps);
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Subscribe a Lambda function to this topic.
|
|
2139
|
+
*/
|
|
2140
|
+
subscribe(name, fn, args) {
|
|
2141
|
+
let subscriptionProps = {
|
|
2142
|
+
filterPolicy: args?.filterPolicy,
|
|
2143
|
+
filterPolicyWithMessageBody: args?.filterPolicyWithMessageBody,
|
|
2144
|
+
deadLetterQueue: args?.deadLetterQueue
|
|
2145
|
+
};
|
|
2146
|
+
if (args?.transform?.subscription) {
|
|
2147
|
+
subscriptionProps = args.transform.subscription(subscriptionProps);
|
|
2148
|
+
}
|
|
2149
|
+
this.topic.addSubscription(new snsSubscriptions.LambdaSubscription(fn, subscriptionProps));
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Subscribe an SQS queue to this topic.
|
|
2153
|
+
*/
|
|
2154
|
+
subscribeQueue(name, queue, args) {
|
|
2155
|
+
let subscriptionProps = {
|
|
2156
|
+
filterPolicy: args?.filterPolicy,
|
|
2157
|
+
filterPolicyWithMessageBody: args?.filterPolicyWithMessageBody,
|
|
2158
|
+
deadLetterQueue: args?.deadLetterQueue,
|
|
2159
|
+
rawMessageDelivery: args?.rawMessageDelivery
|
|
2160
|
+
};
|
|
2161
|
+
if (args?.transform?.subscription) {
|
|
2162
|
+
subscriptionProps = args.transform.subscription(subscriptionProps);
|
|
2163
|
+
}
|
|
2164
|
+
this.topic.addSubscription(new snsSubscriptions.SqsSubscription(queue, subscriptionProps));
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
// src/event-bus.ts
|
|
2169
|
+
import { Construct as Construct19 } from "constructs";
|
|
2170
|
+
import * as events from "aws-cdk-lib/aws-events";
|
|
2171
|
+
import * as targets from "aws-cdk-lib/aws-events-targets";
|
|
2172
|
+
var EventBus2 = class extends Construct19 {
|
|
2173
|
+
bus;
|
|
2174
|
+
get arn() {
|
|
2175
|
+
return this.bus.eventBusArn;
|
|
2176
|
+
}
|
|
2177
|
+
get busName() {
|
|
2178
|
+
return this.bus.eventBusName;
|
|
2179
|
+
}
|
|
2180
|
+
constructor(scope, id, props = {}) {
|
|
2181
|
+
super(scope, id);
|
|
2182
|
+
let busProps = {
|
|
2183
|
+
eventBusName: props.eventBusName,
|
|
2184
|
+
description: props.description
|
|
2185
|
+
};
|
|
2186
|
+
if (props.transform?.bus) {
|
|
2187
|
+
busProps = props.transform.bus(busProps);
|
|
2188
|
+
}
|
|
2189
|
+
this.bus = new events.EventBus(this, "Bus", busProps);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Subscribe a Lambda function to events on this bus.
|
|
2193
|
+
*/
|
|
2194
|
+
subscribe(name, fn, args) {
|
|
2195
|
+
let ruleProps = {
|
|
2196
|
+
eventBus: this.bus,
|
|
2197
|
+
eventPattern: buildEventPattern(args?.pattern)
|
|
2198
|
+
};
|
|
2199
|
+
if (args?.transform?.rule) {
|
|
2200
|
+
ruleProps = args.transform.rule(ruleProps);
|
|
2201
|
+
}
|
|
2202
|
+
const rule = new events.Rule(this, `${name}Rule`, ruleProps);
|
|
2203
|
+
let targetProps = {};
|
|
2204
|
+
if (args?.transform?.target) {
|
|
2205
|
+
targetProps = args.transform.target(targetProps);
|
|
2206
|
+
}
|
|
2207
|
+
rule.addTarget(new targets.LambdaFunction(fn, targetProps));
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Subscribe an SQS queue to events on this bus.
|
|
2211
|
+
*/
|
|
2212
|
+
subscribeQueue(name, queue, args) {
|
|
2213
|
+
let ruleProps = {
|
|
2214
|
+
eventBus: this.bus,
|
|
2215
|
+
eventPattern: buildEventPattern(args?.pattern)
|
|
2216
|
+
};
|
|
2217
|
+
if (args?.transform?.rule) {
|
|
2218
|
+
ruleProps = args.transform.rule(ruleProps);
|
|
2219
|
+
}
|
|
2220
|
+
const rule = new events.Rule(this, `${name}Rule`, ruleProps);
|
|
2221
|
+
let targetProps = {};
|
|
2222
|
+
if (args?.transform?.target) {
|
|
2223
|
+
targetProps = args.transform.target(targetProps);
|
|
2224
|
+
}
|
|
2225
|
+
rule.addTarget(new targets.SqsQueue(queue, targetProps));
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
function buildEventPattern(pattern) {
|
|
2229
|
+
if (!pattern) return void 0;
|
|
2230
|
+
return {
|
|
2231
|
+
source: pattern.source,
|
|
2232
|
+
detailType: pattern.detailType,
|
|
2233
|
+
detail: pattern.detail
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
export {
|
|
2237
|
+
AuroraDatabase,
|
|
2238
|
+
Bucket2 as Bucket,
|
|
2239
|
+
Cluster2 as Cluster,
|
|
2240
|
+
EcsFargateService,
|
|
2241
|
+
Email,
|
|
2242
|
+
EventBus2 as EventBus,
|
|
2243
|
+
LambdaFunction,
|
|
2244
|
+
MySqlDatabase,
|
|
2245
|
+
ReactRouterEcrImage,
|
|
2246
|
+
ReactRouterSsrSiteEcsFargateService,
|
|
2247
|
+
ReactRouterSsrSiteLambda,
|
|
2248
|
+
Redis,
|
|
2249
|
+
SnsTopic,
|
|
2250
|
+
StaticSite,
|
|
2251
|
+
Vpc2 as Vpc
|
|
2252
|
+
};
|