Skip to content

Commit c7490e9

Browse files
committed
chore: wip
chore: wip chore: wip chore: wip
1 parent b7df777 commit c7490e9

File tree

13 files changed

+1014
-270
lines changed

13 files changed

+1014
-270
lines changed

.stacks/core/actions/src/zip/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ await zip(from, to, {
1818
cwd: p.cloudPath('src/runtime'),
1919
})
2020

21-
log.info('zipped your API')
21+
log.info('Zipped your API')

.stacks/core/buddy/src/commands/deploy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export function deploy(buddy: CLI) {
3434
if (await hasUserDomainBeenAddedToCloud(domain)) {
3535
log.success('Your domain is properly configured.')
3636
log.info('Your cloud is deploying...')
37-
console.log(italic('⏳ This may take a while...'))
37+
// eslint-disable-next-line no-console
38+
console.log(`⏳ ${italic('This may take a while...')}`)
3839
await new Promise(resolve => setTimeout(resolve, 2000))
3940
options.domain = domain
4041
}

.stacks/core/cloud/src/cloud/cdn.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-new */
2-
import type { aws_certificatemanager as acm, aws_elasticloadbalancingv2 as elbv2, aws_lambda as lambda, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib'
2+
import type { aws_certificatemanager as acm, aws_lambda as lambda, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib'
33
import {
44
Duration,
55
CfnOutput as Output,
@@ -24,10 +24,8 @@ export interface CdnStackProps extends NestedCloudProps {
2424
firewall: wafv2.CfnWebACL
2525
originRequestFunction: lambda.Function
2626
zone: route53.IHostedZone
27-
lb: elbv2.ApplicationLoadBalancer
2827
}
2928

30-
// export class CdnStack extends NestedStack {
3129
export class CdnStack {
3230
distribution: cloudfront.Distribution
3331
originAccessIdentity: cloudfront.OriginAccessIdentity
@@ -261,16 +259,7 @@ export class CdnStack {
261259
const keysToRemove = ['_HANDLER', '_X_AMZN_TRACE_ID', 'AWS_REGION', 'AWS_EXECUTION_ENV', 'AWS_LAMBDA_FUNCTION_NAME', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 'AWS_LAMBDA_FUNCTION_VERSION', 'AWS_LAMBDA_INITIALIZATION_TYPE', 'AWS_LAMBDA_LOG_GROUP_NAME', 'AWS_LAMBDA_LOG_STREAM_NAME', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_LAMBDA_RUNTIME_API', 'LAMBDA_TASK_ROOT', 'LAMBDA_RUNTIME_DIR', '_']
262260
keysToRemove.forEach(key => delete env[key as EnvKey])
263261

264-
new secretsmanager.Secret(scope, 'StacksSecrets', {
265-
secretName: `${props.appName}-${props.appEnv}-secrets`,
266-
description: 'Secrets for the Stacks application',
267-
generateSecretString: {
268-
secretStringTemplate: JSON.stringify(env),
269-
generateStringKey: Object.keys(env).join(',').length.toString(),
270-
},
271-
})
272-
273-
behaviorOptions = this.apiBehaviorOptions(scope, props)
262+
// behaviorOptions = this.apiBehaviorOptions(scope, props)
274263
}
275264

276265
// if docMode is used, we don't need to add a behavior for the docs
Lines changed: 43 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/* eslint-disable no-new */
2-
import type { aws_certificatemanager as acm, aws_efs as efs } from 'aws-cdk-lib'
3-
import { Duration, CfnOutput as Output, Stack, aws_dynamodb as dynamodb, aws_ec2 as ec2, aws_ecr as ecr, aws_ecs as ecs, aws_elasticloadbalancingv2 as elbv2, aws_iam as iam, aws_logs as logs, aws_route53 as route53, aws_route53_targets as route53Targets } from 'aws-cdk-lib'
2+
import type { aws_certificatemanager as acm, aws_ec2 as ec2, aws_efs as efs, aws_route53 as route53 } from 'aws-cdk-lib'
3+
import { Duration, CfnOutput as Output, aws_ecr_assets as ecr_assets, aws_lambda as lambda, aws_logs as logs, aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
44
import type { Construct } from 'constructs'
5+
import { path as p } from '@stacksjs/path'
6+
import { env } from '@stacksjs/env'
57
import type { NestedCloudProps } from '../types'
8+
import type { EnvKey } from '~/storage/framework/stacks/env'
69

710
export interface ComputeStackProps extends NestedCloudProps {
811
vpc: ec2.Vpc
@@ -12,207 +15,65 @@ export interface ComputeStackProps extends NestedCloudProps {
1215
}
1316

1417
export class ComputeStack {
15-
lb: elbv2.ApplicationLoadBalancer
16-
1718
constructor(scope: Construct, props: ComputeStackProps) {
1819
const vpc = props.vpc
1920
const fileSystem = props.fileSystem
2021

2122
if (!fileSystem)
2223
throw new Error('The file system is missing. Please make sure it was created properly.')
2324

24-
const ecsCluster = new ecs.Cluster(scope, 'DefaultEcsCluster', {
25-
clusterName: `${props.appName}-${props.appEnv}-ecs-cluster`,
26-
containerInsights: true,
27-
vpc,
28-
})
29-
30-
fileSystem.addToResourcePolicy(
31-
new iam.PolicyStatement({
32-
actions: ['elasticfilesystem:ClientMount'],
33-
principals: [new iam.AnyPrincipal()],
34-
conditions: {
35-
Bool: {
36-
'elasticfilesystem:AccessedViaMountTarget': 'true',
37-
},
38-
},
39-
}),
40-
)
41-
42-
const cacheTable = new dynamodb.Table(scope, 'CacheTable', {
43-
partitionKey: { name: 'counter', type: dynamodb.AttributeType.STRING },
44-
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
45-
})
46-
47-
const taskRole = new iam.Role(scope, 'TaskRole', {
48-
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
49-
inlinePolicies: {
50-
AccessToHitCounterTable: new iam.PolicyDocument({
51-
statements: [
52-
new iam.PolicyStatement({
53-
actions: ['dynamodb:Get*', 'dynamodb:UpdateItem'],
54-
resources: [cacheTable.tableArn],
55-
conditions: {
56-
ArnLike: {
57-
'aws:SourceArn': `arn:aws:ecs:${Stack.of(scope).region}:${Stack.of(scope).account}:*`,
58-
},
59-
StringEquals: {
60-
'aws:SourceAccount': Stack.of(scope).account,
61-
},
62-
},
63-
}),
64-
],
65-
}),
66-
},
67-
})
68-
69-
const taskDefinition = new ecs.FargateTaskDefinition(scope, 'FargateTaskDefinition', {
70-
memoryLimitMiB: 512, // TODO: make configurable in cloud.compute
71-
cpu: 256, // TODO: make configurable in cloud.compute
72-
volumes: [
73-
{
74-
name: 'stacks-efs',
75-
efsVolumeConfiguration: {
76-
fileSystemId: fileSystem.fileSystemId,
77-
},
78-
},
79-
],
80-
taskRole,
81-
executionRole: new iam.Role(scope, 'ExecutionRole', {
82-
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
83-
}),
84-
})
85-
86-
const repositoryName = 'stacks-bun-hitcounter' // replace with your repository name
87-
const repository = ecr.Repository.fromRepositoryName(scope, 'ServerRepo', repositoryName)
88-
const containerDef = taskDefinition.addContainer('WebContainer', {
89-
containerName: `${props.appName}-${props.appEnv}-web-container`,
90-
// image: ecs.ContainerImage.fromRegistry('public.ecr.aws/docker/library/nginx:latest'),
91-
image: ecs.ContainerImage.fromEcrRepository(repository),
92-
logging: new ecs.AwsLogDriver({
93-
streamPrefix: `${props.appName}-${props.appEnv}-web`,
94-
logGroup: new logs.LogGroup(scope, 'LogGroup'),
95-
}),
96-
// gpuCount: 0,
97-
})
98-
99-
containerDef.addMountPoints(
100-
{
101-
sourceVolume: 'stacks-efs',
102-
containerPath: '/mnt/efs',
103-
readOnly: false,
104-
},
105-
)
106-
107-
containerDef.addPortMappings({
108-
containerPort: 3000,
109-
hostPort: 3000,
110-
})
111-
112-
const serviceSecurityGroup = new ec2.SecurityGroup(scope, 'ServiceSecurityGroup', {
113-
securityGroupName: `${props.appName}-${props.appEnv}-service-sg`,
114-
vpc,
115-
description: 'Security group for service',
116-
})
117-
118-
const publicLoadBalancerSG = new ec2.SecurityGroup(scope, 'PublicLoadBalancerSG', {
119-
securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
120-
vpc,
121-
description: 'Access to the public facing load balancer',
122-
})
123-
124-
// Assuming serviceSecurityGroup and publicLoadBalancerSG are already defined
125-
serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec2.Port.allTraffic(), 'Ingress from the public ALB')
126-
127-
this.lb = new elbv2.ApplicationLoadBalancer(scope, 'ApplicationLoadBalancer', {
128-
loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
129-
vpc,
130-
vpcSubnets: {
131-
subnets: vpc.selectSubnets({
132-
subnetType: ec2.SubnetType.PUBLIC,
133-
onePerAz: true,
134-
}).subnets,
135-
},
136-
internetFacing: true,
137-
idleTimeout: Duration.seconds(30),
138-
securityGroup: publicLoadBalancerSG,
139-
})
25+
// const dockerImageAsset = new ecr_assets.DockerImageAsset(scope, 'ServerBuildImage', {
26+
// directory: p.cloudPath('src/server'),
27+
// })
14028

141-
const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, 'ServiceTargetGroup', {
142-
// targetGroupName: `${props.appName}-${props.appEnv}-service-tg`,
29+
const webServer = new lambda.Function(scope, 'WebServer', {
30+
// code: lambda.Code.fromDockerBuild(p.cloudPath('src/server')),
31+
description: 'The web server for the Stacks application',
32+
code: lambda.Code.fromAssetImage(p.cloudPath('src/server')),
33+
handler: lambda.Handler.FROM_IMAGE,
34+
runtime: lambda.Runtime.FROM_IMAGE,
14335
vpc,
144-
targetType: elbv2.TargetType.IP,
145-
protocol: elbv2.ApplicationProtocol.HTTP,
146-
port: 3000,
147-
healthCheck: {
148-
interval: Duration.seconds(6),
149-
path: '/',
150-
protocol: elbv2.Protocol.HTTP,
151-
timeout: Duration.seconds(5),
152-
healthyThresholdCount: 2,
153-
unhealthyThresholdCount: 10,
36+
memorySize: 512, // replace with your actual memory size
37+
timeout: Duration.minutes(5), // replace with your actual timeout
38+
logRetention: logs.RetentionDays.ONE_WEEK,
39+
architecture: lambda.Architecture.ARM_64,
40+
// filesystem: lambda.FileSystem.fromEfsAccessPoint(this.storage.accessPoint!, '/mnt/efs'),
41+
})
42+
43+
const keysToRemove = ['_HANDLER', '_X_AMZN_TRACE_ID', 'AWS_REGION', 'AWS_EXECUTION_ENV', 'AWS_LAMBDA_FUNCTION_NAME', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 'AWS_LAMBDA_FUNCTION_VERSION', 'AWS_LAMBDA_INITIALIZATION_TYPE', 'AWS_LAMBDA_LOG_GROUP_NAME', 'AWS_LAMBDA_LOG_STREAM_NAME', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_LAMBDA_RUNTIME_API', 'LAMBDA_TASK_ROOT', 'LAMBDA_RUNTIME_DIR', '_']
44+
keysToRemove.forEach(key => delete env[key as EnvKey])
45+
46+
const secrets = new secretsmanager.Secret(scope, 'StacksSecrets', {
47+
secretName: `${props.appName}-${props.appEnv}-secrets`,
48+
description: 'Secrets for the Stacks application',
49+
generateSecretString: {
50+
secretStringTemplate: JSON.stringify(env),
51+
generateStringKey: Object.keys(env).join(',').length.toString(),
15452
},
15553
})
15654

157-
this.lb.addListener('HttpListener', {
158-
port: 80,
159-
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup]),
160-
})
161-
162-
const service = new ecs.FargateService(scope, 'WebService', {
163-
serviceName: `${props.appName}-${props.appEnv}-web-service`,
164-
cluster: ecsCluster,
165-
taskDefinition,
166-
desiredCount: 2,
167-
assignPublicIp: true,
168-
maxHealthyPercent: 200,
169-
vpcSubnets: vpc.selectSubnets({
170-
subnetType: ec2.SubnetType.PUBLIC,
171-
onePerAz: true,
172-
}),
173-
minHealthyPercent: 75,
174-
securityGroups: [serviceSecurityGroup],
175-
})
176-
177-
service.attachToApplicationTargetGroup(serviceTargetGroup)
178-
179-
publicLoadBalancerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic())
180-
181-
new route53.ARecord(scope, 'AliasApiRecord', {
182-
zone: props.zone,
183-
recordName: 'api',
184-
target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb)),
185-
})
55+
secrets.grantRead(webServer)
56+
webServer.addEnvironment('SECRETS_ARN', secrets.secretArn)
18657

187-
// Setup AutoScaling policy
188-
// TODO: make this configurable in cloud.compute
189-
const scaling = service.autoScaleTaskCount({ maxCapacity: 2 })
190-
scaling.scaleOnCpuUtilization('CpuScaling', {
191-
targetUtilizationPercent: 50,
192-
scaleInCooldown: Duration.seconds(60),
193-
scaleOutCooldown: Duration.seconds(60),
194-
})
195-
scaling.scaleOnMemoryUtilization('MemoryScaling', {
196-
targetUtilizationPercent: 60,
197-
scaleInCooldown: Duration.seconds(60),
198-
scaleOutCooldown: Duration.seconds(60),
58+
const api = new lambda.FunctionUrl(scope, 'StacksServerUrl', {
59+
function: webServer,
60+
authType: lambda.FunctionUrlAuthType.NONE, // becomes a public API
61+
cors: {
62+
allowedOrigins: ['*'],
63+
},
19964
})
20065

201-
// this.compute.fargate.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '0')
202-
203-
// Allow access to EFS from Fargate ECS
204-
fileSystem.grantRootAccess(service.taskDefinition.taskRole.grantPrincipal)
205-
fileSystem.connections.allowDefaultPortFrom(service.connections)
206-
66+
const apiVanityUrl = api.url
20767
const apiPrefix = 'api'
68+
20869
new Output(scope, 'ApiUrl', {
20970
value: `https://${props.domain}/${apiPrefix}`,
21071
description: 'The URL of the deployed application',
21172
})
21273

213-
new Output(scope, 'LoadBalancerDNSName', {
214-
value: `http://${this.lb.loadBalancerDnsName}`,
215-
description: 'The DNS name of the load balancer',
74+
new Output(scope, 'ApiVanityUrl', {
75+
value: apiVanityUrl,
76+
description: 'The Vanity URL of the deployed application',
21677
})
21778
}
21879
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/* eslint-disable no-new */
2+
// import type { aws_lambda as lambda } from 'aws-cdk-lib'
3+
import { Aws, CfnOutput as Output, aws_cloudwatch as cloudwatch } from 'aws-cdk-lib'
4+
import type { Construct } from 'constructs'
5+
import type { NestedCloudProps } from '../types'
6+
7+
export interface DashboardStackProps extends NestedCloudProps {
8+
dashboardName?: string
9+
}
10+
11+
export class DashboardStack {
12+
// lambdaFunction: lambda.Function
13+
dashboard: cloudwatch.Dashboard
14+
15+
constructor(scope: Construct, props: DashboardStackProps) {
16+
const dashboardName = props.dashboardName || 'StacksDashboard'
17+
18+
// Create Sample Lambda Function which will create metrics
19+
// this.lambdaFunction = new Function(this, 'SampleLambda', {
20+
// handler: 'lambda-handler.handler',
21+
// runtime: Runtime.PYTHON_3_7,
22+
// code: new AssetCode(`./lambda`),
23+
// memorySize: 512,
24+
// timeout: Duration.seconds(10),
25+
// })
26+
27+
// Create CloudWatch Dashboard
28+
this.dashboard = new cloudwatch.Dashboard(scope, 'SampleLambdaDashboard', {
29+
dashboardName,
30+
})
31+
32+
// Create Title for Dashboard
33+
this.dashboard.addWidgets(new cloudwatch.TextWidget({
34+
markdown: `# Dashboard: ${this.lambdaFunction.functionName}`,
35+
height: 1,
36+
width: 24,
37+
}))
38+
39+
// Create CloudWatch Dashboard Widgets: Errors, Invocations, Duration, Throttles
40+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
41+
title: 'Invocations',
42+
left: [this.lambdaFunction.metricInvocations()],
43+
width: 24,
44+
}))
45+
46+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
47+
title: 'Errors',
48+
left: [this.lambdaFunction.metricErrors()],
49+
width: 24,
50+
}))
51+
52+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
53+
title: 'Duration',
54+
left: [this.lambdaFunction.metricDuration()],
55+
width: 24,
56+
}))
57+
58+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
59+
title: 'Throttles',
60+
left: [this.lambdaFunction.metricThrottles()],
61+
width: 24,
62+
}))
63+
64+
// Create Widget to show last 20 Log Entries
65+
this.dashboard.addWidgets(new cloudwatch.LogQueryWidget({
66+
logGroupNames: [this.lambdaFunction.logGroup.logGroupName],
67+
queryLines: [
68+
'fields @timestamp, @message',
69+
'sort @timestamp desc',
70+
'limit 20',
71+
],
72+
width: 24,
73+
}))
74+
75+
// Generate Output
76+
const cloudwatchDashboardURL = `https://${Aws.REGION}.console.aws.amazon.com/cloudwatch/home?region=${Aws.REGION}#dashboards:name=${dashboardName}`
77+
78+
new Output(scope, 'DashboardOutput', {
79+
value: cloudwatchDashboardURL,
80+
description: 'The CloudWatch Dashboard URL',
81+
exportName: 'StacksDashboardURL',
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)