@semiont/cli 0.1.0-build.2
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 +497 -0
- package/dist/cli.mjs +45592 -0
- package/dist/dashboard/dashboard.css +238 -0
- package/dist/dashboard/dashboard.js +14 -0
- package/dist/dashboard/dashboard.js.map +7 -0
- package/dist/templates/cdk/app-stack.ts +893 -0
- package/dist/templates/cdk/app.ts +54 -0
- package/dist/templates/cdk/data-stack.ts +416 -0
- package/dist/templates/cdk.json +36 -0
- package/dist/templates/environments/ci.json +52 -0
- package/dist/templates/environments/local.json +82 -0
- package/dist/templates/environments/production.json +57 -0
- package/dist/templates/environments/staging.json +49 -0
- package/dist/templates/environments/test.json +61 -0
- package/dist/templates/inference-claude.json +14 -0
- package/dist/templates/inference-openai.json +16 -0
- package/dist/templates/package.json +23 -0
- package/dist/templates/semiont.json +31 -0
- package/dist/templates/tsconfig.json +27 -0
- package/package.json +91 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
3
|
+
import * as rds from 'aws-cdk-lib/aws-rds';
|
|
4
|
+
import * as ecs from 'aws-cdk-lib/aws-ecs';
|
|
5
|
+
import * as ecr from 'aws-cdk-lib/aws-ecr';
|
|
6
|
+
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
|
|
7
|
+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
|
|
8
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
9
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
10
|
+
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
|
|
11
|
+
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
12
|
+
import * as budgets from 'aws-cdk-lib/aws-budgets';
|
|
13
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
14
|
+
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
15
|
+
import * as route53 from 'aws-cdk-lib/aws-route53';
|
|
16
|
+
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
|
|
17
|
+
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
|
|
18
|
+
import * as efs from 'aws-cdk-lib/aws-efs';
|
|
19
|
+
import { Construct } from 'constructs';
|
|
20
|
+
|
|
21
|
+
interface SemiontAppStackProps extends cdk.StackProps {
|
|
22
|
+
// No longer passing infra resources as props
|
|
23
|
+
// They will be imported via CloudFormation exports
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SemiontAppStack extends cdk.Stack {
|
|
27
|
+
constructor(scope: Construct, id: string, props: SemiontAppStackProps) {
|
|
28
|
+
super(scope, id, props);
|
|
29
|
+
|
|
30
|
+
// Import resources from data stack using CloudFormation exports
|
|
31
|
+
const dataStackName = 'SemiontDataStack';
|
|
32
|
+
|
|
33
|
+
// Import VPC - we need to use fromVpcAttributes since fromLookup doesn't work with tokens
|
|
34
|
+
// We're using 2 AZs, so explicitly specify them
|
|
35
|
+
// Note: CDK will show warnings about missing routeTableIds. These warnings can be ignored
|
|
36
|
+
// as we're importing an existing VPC and not modifying routes. The warnings are due to
|
|
37
|
+
// CDK's limitation when importing VPCs via CloudFormation exports.
|
|
38
|
+
const vpc = ec2.Vpc.fromVpcAttributes(this, 'ImportedVpc', {
|
|
39
|
+
vpcId: cdk.Fn.importValue(`${dataStackName}-VpcId`),
|
|
40
|
+
availabilityZones: ['us-east-2a', 'us-east-2b'], // First 2 AZs in us-east-2
|
|
41
|
+
publicSubnetIds: [
|
|
42
|
+
cdk.Fn.importValue(`${dataStackName}-PublicSubnet1Id`),
|
|
43
|
+
cdk.Fn.importValue(`${dataStackName}-PublicSubnet2Id`),
|
|
44
|
+
],
|
|
45
|
+
privateSubnetIds: [
|
|
46
|
+
cdk.Fn.importValue(`${dataStackName}-PrivateSubnet1Id`),
|
|
47
|
+
cdk.Fn.importValue(`${dataStackName}-PrivateSubnet2Id`),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Import Security Groups
|
|
52
|
+
const dbSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
|
|
53
|
+
this,
|
|
54
|
+
'ImportedDbSecurityGroup',
|
|
55
|
+
cdk.Fn.importValue(`${dataStackName}-DbSecurityGroupId`)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const ecsSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
|
|
59
|
+
this,
|
|
60
|
+
'ImportedEcsSecurityGroup',
|
|
61
|
+
cdk.Fn.importValue(`${dataStackName}-EcsSecurityGroupId`)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const albSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
|
|
65
|
+
this,
|
|
66
|
+
'ImportedAlbSecurityGroup',
|
|
67
|
+
cdk.Fn.importValue(`${dataStackName}-AlbSecurityGroupId`)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Import EFS FileSystem
|
|
71
|
+
const fileSystem = efs.FileSystem.fromFileSystemAttributes(this, 'ImportedFileSystem', {
|
|
72
|
+
fileSystemId: cdk.Fn.importValue(`${dataStackName}-EfsFileSystemId`),
|
|
73
|
+
securityGroup: ecsSecurityGroup,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Import Database (for endpoint reference)
|
|
77
|
+
const databaseEndpoint = cdk.Fn.importValue(`${dataStackName}-DatabaseEndpoint`);
|
|
78
|
+
const databasePort = cdk.Fn.importValue(`${dataStackName}-DatabasePort`);
|
|
79
|
+
|
|
80
|
+
// Import Secrets
|
|
81
|
+
const dbCredentials = secretsmanager.Secret.fromSecretCompleteArn(
|
|
82
|
+
this,
|
|
83
|
+
'ImportedDbCredentials',
|
|
84
|
+
cdk.Fn.importValue(`${dataStackName}-DbCredentialsSecretArn`)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const appSecrets = secretsmanager.Secret.fromSecretCompleteArn(
|
|
88
|
+
this,
|
|
89
|
+
'ImportedAppSecrets',
|
|
90
|
+
cdk.Fn.importValue(`${dataStackName}-AppSecretsSecretArn`)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const jwtSecret = secretsmanager.Secret.fromSecretCompleteArn(
|
|
94
|
+
this,
|
|
95
|
+
'ImportedJwtSecret',
|
|
96
|
+
cdk.Fn.importValue(`${dataStackName}-JwtSecretArn`)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const adminPassword = secretsmanager.Secret.fromSecretCompleteArn(
|
|
100
|
+
this,
|
|
101
|
+
'ImportedAdminPassword',
|
|
102
|
+
cdk.Fn.importValue(`${dataStackName}-AdminPasswordSecretArn`)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const googleOAuth = secretsmanager.Secret.fromSecretCompleteArn(
|
|
106
|
+
this,
|
|
107
|
+
'ImportedGoogleOAuth',
|
|
108
|
+
cdk.Fn.importValue(`${dataStackName}-GoogleOAuthSecretArn`)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const githubOAuth = secretsmanager.Secret.fromSecretCompleteArn(
|
|
112
|
+
this,
|
|
113
|
+
'ImportedGitHubOAuth',
|
|
114
|
+
cdk.Fn.importValue(`${dataStackName}-GitHubOAuthSecretArn`)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const adminEmails = secretsmanager.Secret.fromSecretCompleteArn(
|
|
118
|
+
this,
|
|
119
|
+
'ImportedAdminEmails',
|
|
120
|
+
cdk.Fn.importValue(`${dataStackName}-AdminEmailsSecretArn`)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Get configuration from CDK context
|
|
124
|
+
const siteName = this.node.tryGetContext('siteName') || 'Semiont';
|
|
125
|
+
const domainName = this.node.tryGetContext('domain') || 'example.com';
|
|
126
|
+
const rootDomain = this.node.tryGetContext('rootDomain') || 'example.com';
|
|
127
|
+
const oauthAllowedDomains = this.node.tryGetContext('oauthAllowedDomains') || ['example.com'];
|
|
128
|
+
const databaseName = this.node.tryGetContext('databaseName') || 'semiont';
|
|
129
|
+
const awsCertificateArn = this.node.tryGetContext('certificateArn');
|
|
130
|
+
const awsHostedZoneId = this.node.tryGetContext('hostedZoneId');
|
|
131
|
+
|
|
132
|
+
const certificateArn = new cdk.CfnParameter(this, 'CertificateArn', {
|
|
133
|
+
type: 'String',
|
|
134
|
+
default: awsCertificateArn,
|
|
135
|
+
description: 'ACM Certificate ARN for HTTPS'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const hostedZoneId = new cdk.CfnParameter(this, 'HostedZoneId', {
|
|
139
|
+
type: 'String',
|
|
140
|
+
default: awsHostedZoneId,
|
|
141
|
+
description: 'Route53 Hosted Zone ID'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ECS Cluster with Service Connect
|
|
145
|
+
const cluster = new ecs.Cluster(this, 'SemiontCluster', {
|
|
146
|
+
vpc,
|
|
147
|
+
defaultCloudMapNamespace: {
|
|
148
|
+
name: 'semiont.local',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Enable Container Insights
|
|
153
|
+
cluster.enableFargateCapacityProviders();
|
|
154
|
+
|
|
155
|
+
// CloudWatch Log Group
|
|
156
|
+
const logGroup = new logs.LogGroup(this, 'SemiontLogGroup', {
|
|
157
|
+
retention: logs.RetentionDays.ONE_MONTH,
|
|
158
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Backend Task Definition
|
|
162
|
+
// Note: CDK may show warnings about deprecated inferenceAccelerators property.
|
|
163
|
+
// This is a known CDK bug (https://github.com/aws/aws-cdk/issues/11339) where CDK internally
|
|
164
|
+
// uses a deprecated CloudFormation property. The warning can be safely ignored.
|
|
165
|
+
const backendTaskDefinition = new ecs.FargateTaskDefinition(this, 'SemiontBackendTaskDef', {
|
|
166
|
+
memoryLimitMiB: 512,
|
|
167
|
+
cpu: 256,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Frontend Task Definition
|
|
171
|
+
const frontendTaskDefinition = new ecs.FargateTaskDefinition(this, 'SemiontFrontendTaskDef', {
|
|
172
|
+
memoryLimitMiB: 512,
|
|
173
|
+
cpu: 256,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Add EFS volume to backend task definition for uploads
|
|
177
|
+
const efsVolumeConfig: ecs.EfsVolumeConfiguration = {
|
|
178
|
+
fileSystemId: fileSystem.fileSystemId,
|
|
179
|
+
transitEncryption: 'DISABLED',
|
|
180
|
+
authorizationConfig: {
|
|
181
|
+
iam: 'DISABLED',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
backendTaskDefinition.addVolume({
|
|
186
|
+
name: 'efs-volume',
|
|
187
|
+
efsVolumeConfiguration: efsVolumeConfig,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// IAM role for backend tasks to access Secrets Manager
|
|
191
|
+
backendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
192
|
+
new iam.PolicyStatement({
|
|
193
|
+
effect: iam.Effect.ALLOW,
|
|
194
|
+
actions: [
|
|
195
|
+
'secretsmanager:GetSecretValue',
|
|
196
|
+
],
|
|
197
|
+
resources: [
|
|
198
|
+
dbCredentials.secretArn,
|
|
199
|
+
appSecrets.secretArn,
|
|
200
|
+
jwtSecret.secretArn,
|
|
201
|
+
adminPassword.secretArn,
|
|
202
|
+
googleOAuth.secretArn,
|
|
203
|
+
adminEmails.secretArn,
|
|
204
|
+
],
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// IAM role for frontend tasks (minimal permissions)
|
|
209
|
+
frontendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
210
|
+
new iam.PolicyStatement({
|
|
211
|
+
effect: iam.Effect.ALLOW,
|
|
212
|
+
actions: [
|
|
213
|
+
'secretsmanager:GetSecretValue',
|
|
214
|
+
],
|
|
215
|
+
resources: [
|
|
216
|
+
appSecrets.secretArn,
|
|
217
|
+
googleOAuth.secretArn,
|
|
218
|
+
],
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Add EFS permissions to backend task role
|
|
223
|
+
backendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
224
|
+
new iam.PolicyStatement({
|
|
225
|
+
effect: iam.Effect.ALLOW,
|
|
226
|
+
actions: [
|
|
227
|
+
'elasticfilesystem:ClientMount',
|
|
228
|
+
'elasticfilesystem:ClientWrite',
|
|
229
|
+
'elasticfilesystem:ClientRootAccess',
|
|
230
|
+
],
|
|
231
|
+
resources: [fileSystem.fileSystemArn],
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Add ECS Exec permissions to backend task role
|
|
236
|
+
backendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
237
|
+
new iam.PolicyStatement({
|
|
238
|
+
effect: iam.Effect.ALLOW,
|
|
239
|
+
actions: [
|
|
240
|
+
'ssmmessages:CreateControlChannel',
|
|
241
|
+
'ssmmessages:CreateDataChannel',
|
|
242
|
+
'ssmmessages:OpenControlChannel',
|
|
243
|
+
'ssmmessages:OpenDataChannel',
|
|
244
|
+
],
|
|
245
|
+
resources: ['*'],
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Add Neptune permissions for graph database access
|
|
250
|
+
backendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
251
|
+
new iam.PolicyStatement({
|
|
252
|
+
effect: iam.Effect.ALLOW,
|
|
253
|
+
actions: [
|
|
254
|
+
'neptune-db:*',
|
|
255
|
+
'rds:DescribeDBClusters',
|
|
256
|
+
'rds:DescribeDBInstances',
|
|
257
|
+
],
|
|
258
|
+
resources: ['*'],
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Add ECS Exec permissions to frontend task role
|
|
263
|
+
frontendTaskDefinition.taskRole.addToPrincipalPolicy(
|
|
264
|
+
new iam.PolicyStatement({
|
|
265
|
+
effect: iam.Effect.ALLOW,
|
|
266
|
+
actions: [
|
|
267
|
+
'ssmmessages:CreateControlChannel',
|
|
268
|
+
'ssmmessages:CreateDataChannel',
|
|
269
|
+
'ssmmessages:OpenControlChannel',
|
|
270
|
+
'ssmmessages:OpenDataChannel',
|
|
271
|
+
],
|
|
272
|
+
resources: ['*'],
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Get environment from context
|
|
277
|
+
const environment = this.node.tryGetContext('environment') || 'production';
|
|
278
|
+
|
|
279
|
+
// Backend container - use ECR image or default
|
|
280
|
+
const backendImageUri = this.node.tryGetContext('backendImageUri');
|
|
281
|
+
const backendRepoName = `semiont-backend`;
|
|
282
|
+
const backendImage = backendImageUri
|
|
283
|
+
? ecs.ContainerImage.fromEcrRepository(
|
|
284
|
+
ecr.Repository.fromRepositoryName(this, 'BackendEcrRepo', backendRepoName),
|
|
285
|
+
backendImageUri.split(':')[1] || 'latest'
|
|
286
|
+
)
|
|
287
|
+
: ecs.ContainerImage.fromEcrRepository(
|
|
288
|
+
ecr.Repository.fromRepositoryName(this, 'BackendEcrRepoDefault', backendRepoName),
|
|
289
|
+
'latest'
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const backendContainer = backendTaskDefinition.addContainer('semiont-backend', {
|
|
293
|
+
image: backendImage,
|
|
294
|
+
environment: {
|
|
295
|
+
NODE_ENV: this.node.tryGetContext('nodeEnv') || 'production',
|
|
296
|
+
DEPLOYMENT_VERSION: new Date().toISOString(), // Forces new task definition on every deploy
|
|
297
|
+
DB_HOST: databaseEndpoint,
|
|
298
|
+
DB_PORT: '5432',
|
|
299
|
+
DB_NAME: databaseName,
|
|
300
|
+
PORT: '4000',
|
|
301
|
+
API_PORT: '4000',
|
|
302
|
+
CORS_ORIGIN: `https://${domainName}`,
|
|
303
|
+
FRONTEND_URL: `https://${domainName}`,
|
|
304
|
+
AWS_REGION: this.region,
|
|
305
|
+
// Configuration for OAuth
|
|
306
|
+
SITE_NAME: siteName,
|
|
307
|
+
DOMAIN: domainName,
|
|
308
|
+
OAUTH_ALLOWED_DOMAINS: Array.isArray(oauthAllowedDomains) ? oauthAllowedDomains.join(',') : oauthAllowedDomains,
|
|
309
|
+
SITE_DOMAIN: domainName,
|
|
310
|
+
},
|
|
311
|
+
secrets: {
|
|
312
|
+
DB_USER: ecs.Secret.fromSecretsManager(dbCredentials, 'username'),
|
|
313
|
+
DB_PASSWORD: ecs.Secret.fromSecretsManager(dbCredentials, 'password'),
|
|
314
|
+
JWT_SECRET: ecs.Secret.fromSecretsManager(jwtSecret, 'jwtSecret'),
|
|
315
|
+
SESSION_SECRET: ecs.Secret.fromSecretsManager(appSecrets, 'sessionSecret'),
|
|
316
|
+
GOOGLE_CLIENT_ID: ecs.Secret.fromSecretsManager(googleOAuth, 'clientId'),
|
|
317
|
+
GOOGLE_CLIENT_SECRET: ecs.Secret.fromSecretsManager(googleOAuth, 'clientSecret'),
|
|
318
|
+
},
|
|
319
|
+
logging: ecs.LogDrivers.awsLogs({
|
|
320
|
+
streamPrefix: 'semiont-backend',
|
|
321
|
+
logGroup,
|
|
322
|
+
}),
|
|
323
|
+
healthCheck: {
|
|
324
|
+
command: ['CMD-SHELL', 'curl -f http://localhost:4000/api/health || exit 1'],
|
|
325
|
+
interval: cdk.Duration.seconds(30),
|
|
326
|
+
timeout: cdk.Duration.seconds(5),
|
|
327
|
+
retries: 3,
|
|
328
|
+
startPeriod: cdk.Duration.minutes(1),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
backendContainer.addPortMappings({
|
|
333
|
+
containerPort: 4000,
|
|
334
|
+
name: 'backend', // This name must match the portMappingName in Service Connect config
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Mount EFS volume for uploads
|
|
338
|
+
backendContainer.addMountPoints({
|
|
339
|
+
sourceVolume: 'efs-volume',
|
|
340
|
+
containerPath: '/app/uploads',
|
|
341
|
+
readOnly: false,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Frontend container - use ECR image or default
|
|
345
|
+
const frontendImageUri = this.node.tryGetContext('frontendImageUri');
|
|
346
|
+
const frontendRepoName = `semiont-frontend`;
|
|
347
|
+
const frontendImage = frontendImageUri
|
|
348
|
+
? ecs.ContainerImage.fromEcrRepository(
|
|
349
|
+
ecr.Repository.fromRepositoryName(this, 'FrontendEcrRepo', frontendRepoName),
|
|
350
|
+
frontendImageUri.split(':')[1] || 'latest'
|
|
351
|
+
)
|
|
352
|
+
: ecs.ContainerImage.fromEcrRepository(
|
|
353
|
+
ecr.Repository.fromRepositoryName(this, 'FrontendEcrRepoDefault', frontendRepoName),
|
|
354
|
+
'latest'
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const frontendContainer = frontendTaskDefinition.addContainer('semiont-frontend', {
|
|
358
|
+
image: frontendImage,
|
|
359
|
+
environment: {
|
|
360
|
+
NODE_ENV: this.node.tryGetContext('nodeEnv') || 'production',
|
|
361
|
+
DEPLOYMENT_VERSION: new Date().toISOString(), // Forces new task definition on every deploy
|
|
362
|
+
PORT: '3000',
|
|
363
|
+
HOSTNAME: '0.0.0.0',
|
|
364
|
+
// Public environment variables (available to browser)
|
|
365
|
+
NEXT_PUBLIC_API_URL: `https://${domainName}`,
|
|
366
|
+
NEXT_PUBLIC_SITE_NAME: siteName,
|
|
367
|
+
NEXT_PUBLIC_DOMAIN: domainName,
|
|
368
|
+
// OAuth domains for server-side NextAuth validation
|
|
369
|
+
OAUTH_ALLOWED_DOMAINS: Array.isArray(oauthAllowedDomains) ? oauthAllowedDomains.join(',') : oauthAllowedDomains,
|
|
370
|
+
// NextAuth configuration
|
|
371
|
+
NEXTAUTH_URL: `https://${domainName}`,
|
|
372
|
+
// Backend URL for server-side authentication calls (Service Connect DNS)
|
|
373
|
+
// Used by frontend's NextAuth to communicate with backend container-to-container
|
|
374
|
+
// This is NOT the public URL - it's the internal AWS Service Connect address
|
|
375
|
+
BACKEND_INTERNAL_URL: 'http://backend:4000',
|
|
376
|
+
},
|
|
377
|
+
secrets: {
|
|
378
|
+
NEXTAUTH_SECRET: ecs.Secret.fromSecretsManager(appSecrets, 'nextAuthSecret'),
|
|
379
|
+
GOOGLE_CLIENT_ID: ecs.Secret.fromSecretsManager(googleOAuth, 'clientId'),
|
|
380
|
+
GOOGLE_CLIENT_SECRET: ecs.Secret.fromSecretsManager(googleOAuth, 'clientSecret'),
|
|
381
|
+
},
|
|
382
|
+
logging: ecs.LogDrivers.awsLogs({
|
|
383
|
+
streamPrefix: 'semiont-frontend',
|
|
384
|
+
logGroup,
|
|
385
|
+
}),
|
|
386
|
+
healthCheck: {
|
|
387
|
+
command: ['CMD-SHELL', 'curl -f http://localhost:3000/ || exit 1'],
|
|
388
|
+
interval: cdk.Duration.seconds(30),
|
|
389
|
+
timeout: cdk.Duration.seconds(5),
|
|
390
|
+
retries: 3,
|
|
391
|
+
startPeriod: cdk.Duration.minutes(1),
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
frontendContainer.addPortMappings({
|
|
396
|
+
containerPort: 3000,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Backend ECS Service
|
|
400
|
+
const backendService = new ecs.FargateService(this, 'SemiontBackendService', {
|
|
401
|
+
cluster,
|
|
402
|
+
taskDefinition: backendTaskDefinition,
|
|
403
|
+
desiredCount: 1,
|
|
404
|
+
vpcSubnets: {
|
|
405
|
+
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
406
|
+
},
|
|
407
|
+
securityGroups: [ecsSecurityGroup],
|
|
408
|
+
assignPublicIp: false,
|
|
409
|
+
healthCheckGracePeriod: cdk.Duration.minutes(2),
|
|
410
|
+
enableExecuteCommand: true,
|
|
411
|
+
circuitBreaker: {
|
|
412
|
+
rollback: true,
|
|
413
|
+
},
|
|
414
|
+
minHealthyPercent: 100,
|
|
415
|
+
maxHealthyPercent: 200,
|
|
416
|
+
serviceConnectConfiguration: {
|
|
417
|
+
services: [
|
|
418
|
+
{
|
|
419
|
+
portMappingName: 'backend',
|
|
420
|
+
dnsName: 'backend',
|
|
421
|
+
port: 4000,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Frontend ECS Service
|
|
428
|
+
const frontendService = new ecs.FargateService(this, 'SemiontFrontendService', {
|
|
429
|
+
cluster,
|
|
430
|
+
taskDefinition: frontendTaskDefinition,
|
|
431
|
+
desiredCount: 1,
|
|
432
|
+
vpcSubnets: {
|
|
433
|
+
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
434
|
+
},
|
|
435
|
+
securityGroups: [ecsSecurityGroup],
|
|
436
|
+
assignPublicIp: false,
|
|
437
|
+
healthCheckGracePeriod: cdk.Duration.minutes(2),
|
|
438
|
+
enableExecuteCommand: true,
|
|
439
|
+
circuitBreaker: {
|
|
440
|
+
rollback: true,
|
|
441
|
+
},
|
|
442
|
+
minHealthyPercent: 100,
|
|
443
|
+
maxHealthyPercent: 200,
|
|
444
|
+
serviceConnectConfiguration: {
|
|
445
|
+
// Frontend is a client-only service, it discovers backend but doesn't expose itself
|
|
446
|
+
services: [],
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Auto Scaling for Backend
|
|
451
|
+
const backendScaling = backendService.autoScaleTaskCount({
|
|
452
|
+
minCapacity: 1,
|
|
453
|
+
maxCapacity: 10,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
backendScaling.scaleOnCpuUtilization('BackendCpuScaling', {
|
|
457
|
+
targetUtilizationPercent: 70,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
backendScaling.scaleOnMemoryUtilization('BackendMemoryScaling', {
|
|
461
|
+
targetUtilizationPercent: 80,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Auto Scaling for Frontend
|
|
465
|
+
const frontendScaling = frontendService.autoScaleTaskCount({
|
|
466
|
+
minCapacity: 1,
|
|
467
|
+
maxCapacity: 5,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
frontendScaling.scaleOnCpuUtilization('FrontendCpuScaling', {
|
|
471
|
+
targetUtilizationPercent: 70,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
frontendScaling.scaleOnMemoryUtilization('FrontendMemoryScaling', {
|
|
475
|
+
targetUtilizationPercent: 80,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Route 53 Hosted Zone
|
|
479
|
+
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
|
|
480
|
+
hostedZoneId: hostedZoneId.valueAsString,
|
|
481
|
+
zoneName: rootDomain,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// SSL Certificate
|
|
485
|
+
const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate',
|
|
486
|
+
certificateArn.valueAsString
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Application Load Balancer
|
|
490
|
+
const alb = new elbv2.ApplicationLoadBalancer(this, 'SemiontALB', {
|
|
491
|
+
vpc,
|
|
492
|
+
internetFacing: true,
|
|
493
|
+
securityGroup: albSecurityGroup,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// HTTPS Listener
|
|
497
|
+
const httpsListener = alb.addListener('HttpsListener', {
|
|
498
|
+
port: 443,
|
|
499
|
+
open: true,
|
|
500
|
+
certificates: [certificate],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// NextAuth routes (Priority 10: /api/auth/* -> Frontend)
|
|
504
|
+
httpsListener.addTargets('NextAuth', {
|
|
505
|
+
port: 3000,
|
|
506
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
507
|
+
targets: [frontendService],
|
|
508
|
+
conditions: [
|
|
509
|
+
elbv2.ListenerCondition.pathPatterns(['/api/auth/*']),
|
|
510
|
+
],
|
|
511
|
+
healthCheck: {
|
|
512
|
+
path: '/',
|
|
513
|
+
interval: cdk.Duration.seconds(30),
|
|
514
|
+
timeout: cdk.Duration.seconds(10),
|
|
515
|
+
healthyThresholdCount: 2,
|
|
516
|
+
unhealthyThresholdCount: 5,
|
|
517
|
+
healthyHttpCodes: '200',
|
|
518
|
+
},
|
|
519
|
+
priority: 10,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Backend API (Priority 20: /api/* -> Backend)
|
|
523
|
+
httpsListener.addTargets('BackendAPI', {
|
|
524
|
+
port: 4000,
|
|
525
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
526
|
+
targets: [backendService],
|
|
527
|
+
conditions: [
|
|
528
|
+
elbv2.ListenerCondition.pathPatterns([
|
|
529
|
+
'/api', // API root
|
|
530
|
+
'/api/*' // All API routes
|
|
531
|
+
]),
|
|
532
|
+
],
|
|
533
|
+
healthCheck: {
|
|
534
|
+
path: '/api/health',
|
|
535
|
+
interval: cdk.Duration.seconds(30),
|
|
536
|
+
timeout: cdk.Duration.seconds(10),
|
|
537
|
+
healthyThresholdCount: 2,
|
|
538
|
+
unhealthyThresholdCount: 5,
|
|
539
|
+
healthyHttpCodes: '200',
|
|
540
|
+
},
|
|
541
|
+
priority: 20,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Frontend target group (default action for all other paths)
|
|
545
|
+
httpsListener.addTargets('Frontend', {
|
|
546
|
+
port: 3000,
|
|
547
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
548
|
+
targets: [frontendService],
|
|
549
|
+
healthCheck: {
|
|
550
|
+
path: '/',
|
|
551
|
+
interval: cdk.Duration.seconds(30),
|
|
552
|
+
timeout: cdk.Duration.seconds(10),
|
|
553
|
+
healthyThresholdCount: 2,
|
|
554
|
+
unhealthyThresholdCount: 5,
|
|
555
|
+
healthyHttpCodes: '200',
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// HTTP Listener (redirect to HTTPS)
|
|
560
|
+
alb.addListener('Listener', {
|
|
561
|
+
port: 80,
|
|
562
|
+
open: true,
|
|
563
|
+
defaultAction: elbv2.ListenerAction.redirect({
|
|
564
|
+
protocol: 'HTTPS',
|
|
565
|
+
port: '443',
|
|
566
|
+
permanent: true,
|
|
567
|
+
}),
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Route 53 Record
|
|
571
|
+
new route53.ARecord(this, 'SemiontARecord', {
|
|
572
|
+
zone: hostedZone,
|
|
573
|
+
recordName: 'wiki',
|
|
574
|
+
target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// SNS Topic for alerts
|
|
578
|
+
const alertTopic = new sns.Topic(this, 'SemiontAlerts', {
|
|
579
|
+
displayName: 'Semiont Alerts',
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// CloudWatch Alarms for Backend
|
|
583
|
+
const backendCpuAlarm = new cloudwatch.Alarm(this, 'BackendHighCPUAlarm', {
|
|
584
|
+
metric: backendService.metricCpuUtilization(),
|
|
585
|
+
threshold: 80,
|
|
586
|
+
evaluationPeriods: 2,
|
|
587
|
+
datapointsToAlarm: 2,
|
|
588
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
backendCpuAlarm.addAlarmAction(
|
|
592
|
+
new cloudwatchActions.SnsAction(alertTopic)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const backendMemoryAlarm = new cloudwatch.Alarm(this, 'BackendHighMemoryAlarm', {
|
|
596
|
+
metric: backendService.metricMemoryUtilization(),
|
|
597
|
+
threshold: 85,
|
|
598
|
+
evaluationPeriods: 2,
|
|
599
|
+
datapointsToAlarm: 2,
|
|
600
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
backendMemoryAlarm.addAlarmAction(
|
|
604
|
+
new cloudwatchActions.SnsAction(alertTopic)
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// CloudWatch Alarms for Frontend
|
|
608
|
+
const frontendCpuAlarm = new cloudwatch.Alarm(this, 'FrontendHighCPUAlarm', {
|
|
609
|
+
metric: frontendService.metricCpuUtilization(),
|
|
610
|
+
threshold: 80,
|
|
611
|
+
evaluationPeriods: 2,
|
|
612
|
+
datapointsToAlarm: 2,
|
|
613
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
frontendCpuAlarm.addAlarmAction(
|
|
617
|
+
new cloudwatchActions.SnsAction(alertTopic)
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const frontendMemoryAlarm = new cloudwatch.Alarm(this, 'FrontendHighMemoryAlarm', {
|
|
621
|
+
metric: frontendService.metricMemoryUtilization(),
|
|
622
|
+
threshold: 85,
|
|
623
|
+
evaluationPeriods: 2,
|
|
624
|
+
datapointsToAlarm: 2,
|
|
625
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
frontendMemoryAlarm.addAlarmAction(
|
|
629
|
+
new cloudwatchActions.SnsAction(alertTopic)
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// Cost Budget
|
|
633
|
+
new budgets.CfnBudget(this, 'MonthlyBudget', {
|
|
634
|
+
budget: {
|
|
635
|
+
budgetName: 'Semiont Monthly Budget',
|
|
636
|
+
budgetLimit: {
|
|
637
|
+
amount: 200,
|
|
638
|
+
unit: 'USD',
|
|
639
|
+
},
|
|
640
|
+
timeUnit: 'MONTHLY',
|
|
641
|
+
budgetType: 'COST',
|
|
642
|
+
},
|
|
643
|
+
notificationsWithSubscribers: [
|
|
644
|
+
{
|
|
645
|
+
notification: {
|
|
646
|
+
notificationType: 'ACTUAL',
|
|
647
|
+
comparisonOperator: 'GREATER_THAN',
|
|
648
|
+
threshold: 80,
|
|
649
|
+
thresholdType: 'PERCENTAGE',
|
|
650
|
+
},
|
|
651
|
+
subscribers: [
|
|
652
|
+
{
|
|
653
|
+
subscriptionType: 'SNS',
|
|
654
|
+
address: alertTopic.topicArn,
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
notification: {
|
|
660
|
+
notificationType: 'FORECASTED',
|
|
661
|
+
comparisonOperator: 'GREATER_THAN',
|
|
662
|
+
threshold: 100,
|
|
663
|
+
thresholdType: 'PERCENTAGE',
|
|
664
|
+
},
|
|
665
|
+
subscribers: [
|
|
666
|
+
{
|
|
667
|
+
subscriptionType: 'SNS',
|
|
668
|
+
address: alertTopic.topicArn,
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// WAF Web ACL with enhanced exclusions for uploads
|
|
676
|
+
const webAcl = new wafv2.CfnWebACL(this, 'SemiontWAF', {
|
|
677
|
+
scope: 'REGIONAL',
|
|
678
|
+
defaultAction: { allow: {} },
|
|
679
|
+
rules: [
|
|
680
|
+
// Allow MCP OAuth callbacks with localhost (before other rules)
|
|
681
|
+
{
|
|
682
|
+
name: 'AllowMCPCallbacks',
|
|
683
|
+
priority: 0,
|
|
684
|
+
action: { allow: {} },
|
|
685
|
+
statement: {
|
|
686
|
+
andStatement: {
|
|
687
|
+
statements: [
|
|
688
|
+
{
|
|
689
|
+
byteMatchStatement: {
|
|
690
|
+
searchString: '/auth/mcp-setup',
|
|
691
|
+
fieldToMatch: { uriPath: {} },
|
|
692
|
+
textTransformations: [{ priority: 0, type: 'LOWERCASE' }],
|
|
693
|
+
positionalConstraint: 'STARTS_WITH'
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
orStatement: {
|
|
698
|
+
statements: [
|
|
699
|
+
{
|
|
700
|
+
byteMatchStatement: {
|
|
701
|
+
searchString: 'localhost',
|
|
702
|
+
fieldToMatch: { queryString: {} },
|
|
703
|
+
textTransformations: [{ priority: 0, type: 'LOWERCASE' }],
|
|
704
|
+
positionalConstraint: 'CONTAINS'
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
byteMatchStatement: {
|
|
709
|
+
searchString: '127.0.0.1',
|
|
710
|
+
fieldToMatch: { queryString: {} },
|
|
711
|
+
textTransformations: [{ priority: 0, type: 'NONE' }],
|
|
712
|
+
positionalConstraint: 'CONTAINS'
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
visibilityConfig: {
|
|
722
|
+
sampledRequestsEnabled: true,
|
|
723
|
+
cloudWatchMetricsEnabled: true,
|
|
724
|
+
metricName: 'MCPCallbackAllowMetric',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: 'AWSManagedRulesCommonRuleSet',
|
|
729
|
+
priority: 10,
|
|
730
|
+
overrideAction: { none: {} },
|
|
731
|
+
statement: {
|
|
732
|
+
managedRuleGroupStatement: {
|
|
733
|
+
vendorName: 'AWS',
|
|
734
|
+
name: 'AWSManagedRulesCommonRuleSet',
|
|
735
|
+
excludedRules: [
|
|
736
|
+
{ name: 'SizeRestrictions_BODY' },
|
|
737
|
+
{ name: 'GenericRFI_BODY' },
|
|
738
|
+
{ name: 'GenericRFI_QUERYARGUMENTS' },
|
|
739
|
+
{ name: 'GenericRFI_URIPATH' },
|
|
740
|
+
{ name: 'CrossSiteScripting_BODY' },
|
|
741
|
+
{ name: 'RestrictedExtensions_URIPATH' },
|
|
742
|
+
{ name: 'EC2MetaDataSSRF_BODY' },
|
|
743
|
+
{ name: 'NoUserAgent_HEADER' },
|
|
744
|
+
],
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
visibilityConfig: {
|
|
748
|
+
sampledRequestsEnabled: true,
|
|
749
|
+
cloudWatchMetricsEnabled: true,
|
|
750
|
+
metricName: 'CommonRuleSetMetric',
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: 'AWSManagedRulesKnownBadInputsRuleSet',
|
|
755
|
+
priority: 20,
|
|
756
|
+
overrideAction: { none: {} },
|
|
757
|
+
statement: {
|
|
758
|
+
managedRuleGroupStatement: {
|
|
759
|
+
vendorName: 'AWS',
|
|
760
|
+
name: 'AWSManagedRulesKnownBadInputsRuleSet',
|
|
761
|
+
excludedRules: [
|
|
762
|
+
{ name: 'Host_localhost_HEADER' },
|
|
763
|
+
{ name: 'PROPFIND_METHOD' },
|
|
764
|
+
{ name: 'ExploitablePaths_URIPATH' },
|
|
765
|
+
],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
visibilityConfig: {
|
|
769
|
+
sampledRequestsEnabled: true,
|
|
770
|
+
cloudWatchMetricsEnabled: true,
|
|
771
|
+
metricName: 'KnownBadInputsRuleSetMetric',
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: 'RateLimitRule',
|
|
776
|
+
priority: 30,
|
|
777
|
+
action: { block: {} },
|
|
778
|
+
statement: {
|
|
779
|
+
rateBasedStatement: {
|
|
780
|
+
limit: 2000,
|
|
781
|
+
aggregateKeyType: 'IP',
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
visibilityConfig: {
|
|
785
|
+
sampledRequestsEnabled: true,
|
|
786
|
+
cloudWatchMetricsEnabled: true,
|
|
787
|
+
metricName: 'RateLimitMetric',
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
visibilityConfig: {
|
|
792
|
+
sampledRequestsEnabled: true,
|
|
793
|
+
cloudWatchMetricsEnabled: true,
|
|
794
|
+
metricName: 'SemiontWAF',
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// WAF association with ALB
|
|
799
|
+
new wafv2.CfnWebACLAssociation(this, 'WAFAssociation', {
|
|
800
|
+
resourceArn: alb.loadBalancerArn,
|
|
801
|
+
webAclArn: webAcl.attrArn,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// CloudWatch Dashboard
|
|
805
|
+
const dashboard = new cloudwatch.Dashboard(this, 'SemiontDashboard', {
|
|
806
|
+
dashboardName: 'Semiont-Monitoring',
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
dashboard.addWidgets(
|
|
810
|
+
new cloudwatch.GraphWidget({
|
|
811
|
+
title: 'Backend Service Metrics',
|
|
812
|
+
left: [backendService.metricCpuUtilization(), backendService.metricMemoryUtilization()],
|
|
813
|
+
width: 12,
|
|
814
|
+
}),
|
|
815
|
+
new cloudwatch.GraphWidget({
|
|
816
|
+
title: 'Frontend Service Metrics',
|
|
817
|
+
left: [frontendService.metricCpuUtilization(), frontendService.metricMemoryUtilization()],
|
|
818
|
+
width: 12,
|
|
819
|
+
}),
|
|
820
|
+
new cloudwatch.GraphWidget({
|
|
821
|
+
title: 'ALB Metrics',
|
|
822
|
+
left: [alb.metrics.requestCount(), alb.metrics.targetResponseTime()],
|
|
823
|
+
width: 12,
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// Outputs
|
|
828
|
+
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
|
|
829
|
+
value: alb.loadBalancerDnsName,
|
|
830
|
+
description: 'Application Load Balancer DNS Name',
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
new cdk.CfnOutput(this, 'SNSTopicArn', {
|
|
834
|
+
value: alertTopic.topicArn,
|
|
835
|
+
description: 'SNS Topic for alerts',
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
new cdk.CfnOutput(this, 'BackendTaskDefinitionArn', {
|
|
839
|
+
value: backendTaskDefinition.taskDefinitionArn,
|
|
840
|
+
description: 'Backend Task Definition ARN',
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
new cdk.CfnOutput(this, 'FrontendTaskDefinitionArn', {
|
|
844
|
+
value: frontendTaskDefinition.taskDefinitionArn,
|
|
845
|
+
description: 'Frontend Task Definition ARN',
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
new cdk.CfnOutput(this, 'ClusterName', {
|
|
849
|
+
value: cluster.clusterName,
|
|
850
|
+
description: 'ECS Cluster name',
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
new cdk.CfnOutput(this, 'BackendServiceName', {
|
|
854
|
+
value: backendService.serviceName,
|
|
855
|
+
description: 'Backend ECS Service name',
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
new cdk.CfnOutput(this, 'BackendServiceArn', {
|
|
859
|
+
value: backendService.serviceArn,
|
|
860
|
+
description: 'Backend ECS Service ARN',
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
new cdk.CfnOutput(this, 'FrontendServiceName', {
|
|
864
|
+
value: frontendService.serviceName,
|
|
865
|
+
description: 'Frontend ECS Service name',
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
new cdk.CfnOutput(this, 'FrontendServiceArn', {
|
|
869
|
+
value: frontendService.serviceArn,
|
|
870
|
+
description: 'Frontend ECS Service ARN',
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
new cdk.CfnOutput(this, 'LogGroupName', {
|
|
874
|
+
value: logGroup.logGroupName,
|
|
875
|
+
description: 'CloudWatch Log Group name',
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
new cdk.CfnOutput(this, 'CustomDomainUrl', {
|
|
879
|
+
value: `https://${domainName}`,
|
|
880
|
+
description: 'Semiont Custom Domain URL',
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
new cdk.CfnOutput(this, 'WAFWebACLArn', {
|
|
884
|
+
value: webAcl.attrArn,
|
|
885
|
+
description: 'WAF Web ACL ARN',
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
new cdk.CfnOutput(this, 'SiteName', {
|
|
889
|
+
value: siteName,
|
|
890
|
+
description: 'Semiont Site Name',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|