Skip to content

Commit 8599871

Browse files
committed
chore: wip
1 parent 7f82a53 commit 8599871

File tree

10 files changed

+254
-52
lines changed

10 files changed

+254
-52
lines changed

.stacks/core/cloud/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@aws-sdk/client-cloudformation": "^3.433.0",
6161
"@aws-sdk/client-cloudfront": "^3.433.0",
6262
"@aws-sdk/client-cloudwatch-logs": "^3.433.0",
63+
"@aws-sdk/client-dynamodb": "3.433.0",
6364
"@aws-sdk/client-ec2": "^3.433.0",
6465
"@aws-sdk/client-efs": "^3.433.0",
6566
"@aws-sdk/client-iam": "^3.433.0",
@@ -69,6 +70,7 @@
6970
"@aws-sdk/client-ses": "^3.433.0",
7071
"@aws-sdk/client-sesv2": "^3.433.0",
7172
"@aws-sdk/client-ssm": "^3.433.0",
73+
"@aws-sdk/lib-dynamodb": "^3.433.0",
7274
"@stacksjs/config": "workspace:*",
7375
"@stacksjs/logging": "workspace:*",
7476
"@stacksjs/path": "workspace:*",

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

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { string } from '@stacksjs/strings'
99
import type { CfnResource, StackProps } from 'aws-cdk-lib'
1010
import {
1111
AssetHashType,
12+
CfnParameter,
1213
Duration,
1314
Fn,
1415
CfnOutput as Output,
@@ -19,12 +20,15 @@ import {
1920
aws_certificatemanager as acm,
2021
aws_backup as backup,
2122
aws_cloudfront as cloudfront,
23+
aws_dynamodb as dynamodb,
2224
aws_ec2 as ec2,
25+
aws_ecs as ecs,
2326
aws_efs as efs,
27+
aws_elasticloadbalancingv2 as elbv2,
2428
aws_iam as iam,
25-
// custom_resources,
2629
aws_kms as kms,
2730
aws_lambda as lambda,
31+
aws_logs as logs,
2832
aws_cloudfront_origins as origins,
2933
aws_route53 as route53,
3034
aws_s3 as s3,
@@ -127,12 +131,9 @@ export class StacksCloud extends Stack {
127131
await this.manageStorage()
128132
this.manageFirewall()
129133
this.manageFileSystem()
130-
131-
// this also deploys your API
132134
this.manageCdn()
133-
134-
// needs to run after the cdn is created because it links the distribution
135-
this.manageDns()
135+
this.manageCompute()
136+
this.manageDns() // needs to run after the cdn is created because it links the distribution
136137
this.deploy()
137138
this.addOutputs()
138139
}
@@ -284,6 +285,132 @@ export class StacksCloud extends Stack {
284285
}
285286
}
286287

288+
manageCompute() {
289+
// ECS Cluster
290+
const cluster = new ecs.Cluster(this, 'ECSCluster', {
291+
clusterName: `${this.appName}-${appEnv}-ecs-cluster`,
292+
containerInsights: true,
293+
vpc: this.vpc,
294+
})
295+
296+
const vpc = this.vpc
297+
298+
// ECS Task Execution Role
299+
const ecsTaskExecutionRole = new iam.Role(this, 'ECSTaskExecutionRole', {
300+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
301+
managedPolicies: [
302+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
303+
],
304+
})
305+
306+
// Task Definition
307+
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
308+
cpu: 256, // TODO: make this configurable
309+
memoryLimitMiB: 512, // TODO: make this configurable
310+
executionRole: ecsTaskExecutionRole,
311+
})
312+
313+
const container = taskDefinition.addContainer('web', {
314+
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/docker/library/nginx:latest'),
315+
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'ServiceName' }),
316+
})
317+
318+
container.addPortMappings({ containerPort: 3000 })
319+
320+
// Security Group
321+
const securityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
322+
vpc,
323+
description: 'Security group for service',
324+
})
325+
326+
// Load Balancer
327+
const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'PublicLoadBalancer', {
328+
loadBalancerName: `${this.appName}-${appEnv}-load-balancer`,
329+
vpc,
330+
internetFacing: true,
331+
securityGroup,
332+
333+
})
334+
335+
const listener = loadBalancer.addListener('PublicLoadBalancerListener', {
336+
port: 80,
337+
protocol: elbv2.ApplicationProtocol.HTTP,
338+
})
339+
340+
// Target Group
341+
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'ServiceTargetGroup', {
342+
vpc,
343+
port: 3000,
344+
protocol: elbv2.ApplicationProtocol.HTTP,
345+
targetType: elbv2.TargetType.IP,
346+
healthCheck: {
347+
path: '/',
348+
interval: Duration.seconds(6),
349+
timeout: Duration.seconds(5),
350+
healthyThresholdCount: 2,
351+
unhealthyThresholdCount: 10,
352+
},
353+
})
354+
355+
// ECS Service
356+
const service = new ecs.FargateService(this, 'WebService', {
357+
serviceName: `${this.appName}-${appEnv}-web-service`,
358+
cluster,
359+
taskDefinition,
360+
desiredCount: 2,
361+
securityGroups: [securityGroup],
362+
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
363+
assignPublicIp: true,
364+
})
365+
// Add the Fargate Service to the Target Group
366+
targetGroup.addTarget(service)
367+
368+
// Add the Target Group to the Listener
369+
listener.addTargetGroups('ServiceTarget', {
370+
targetGroups: [targetGroup],
371+
})
372+
373+
service.connections.allowFrom(loadBalancer, ec2.Port.allTraffic())
374+
375+
const table = new dynamodb.Table(this, 'HitCounters', {
376+
partitionKey: { name: 'counter', type: dynamodb.AttributeType.STRING },
377+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
378+
})
379+
380+
listener.addTargets('ServiceTarget', {
381+
targetGroupName: `${this.appName}-${appEnv}-service-target`,
382+
targets: [service],
383+
})
384+
385+
// Task Role
386+
const taskRole = new iam.Role(this, 'TaskRole', {
387+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
388+
})
389+
390+
taskRole.addToPolicy(new iam.PolicyStatement({
391+
actions: ['dynamodb:Get*', 'dynamodb:UpdateItem'],
392+
resources: [table.tableArn],
393+
}))
394+
395+
// Log Group
396+
new logs.LogGroup(this, 'ServerLogGroup', {
397+
logGroupName: `${this.appName}-${appEnv}-server`,
398+
retention: logs.RetentionDays.ONE_WEEK,
399+
removalPolicy: RemovalPolicy.DESTROY,
400+
})
401+
402+
// Parameters
403+
new CfnParameter(this, 'ContainerImageUrl', {
404+
type: 'String',
405+
description: 'The URL of the container image that you built',
406+
})
407+
408+
new Output(this, 'ComputeClusterName', {
409+
value: cluster.clusterName,
410+
description: 'The ECS cluster into which to launch resources',
411+
})
412+
}
413+
287414
manageDns() {
288415
// Create a Route53 record pointing to the CloudFront distribution
289416
new route53.ARecord(this, 'AliasRecord', {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM oven/bun
2+
WORKDIR /srv
3+
4+
# Add the package manifest and install packages
5+
ADD package.json .
6+
RUN bun install
7+
8+
# Add the application code
9+
ADD index.ts .
10+
11+
# Specify the command to run when launching the container
12+
CMD bun index.ts
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import process from 'node:process'
2+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
3+
import type { UpdateCommandOutput } from '@aws-sdk/lib-dynamodb'
4+
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb'
5+
6+
// Bare-bones DynamoDB Client
7+
const dynamodb = new DynamoDBClient()
8+
9+
// Bare-bones document client
10+
const db = DynamoDBDocumentClient.from(dynamodb)
11+
12+
const TABLE_NAME = process.env.TABLE_NAME
13+
14+
if (!TABLE_NAME)
15+
throw new Error('Expected environment variable `TABLE_NAME` with name of DynamoDB table to store counter in')
16+
17+
const server = Bun.serve({
18+
port: 3000,
19+
async fetch(req) {
20+
const command = new UpdateCommand({
21+
TableName: TABLE_NAME,
22+
UpdateExpression: 'ADD hitCount :increment',
23+
ExpressionAttributeValues: {
24+
':increment': 1,
25+
},
26+
Key: {
27+
counter: 'global',
28+
},
29+
ReturnValues: 'ALL_NEW',
30+
})
31+
32+
const resp = await db.send(command) as UpdateCommandOutput
33+
34+
if (resp.Attributes)
35+
return new Response(resp.Attributes.hitCount)
36+
37+
else
38+
return new Response(JSON.stringify(resp))
39+
},
40+
})
41+
42+
// eslint-disable-next-line no-console
43+
console.log(`Listening on http://localhost:${server.port} ...`)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
REPO_URI=$(aws ecr create-repository --repository-name stacks-bun-hitcounter --query 'repository.repositoryUri' --output text)
2+
docker build -t $REPO_URI .
3+
docker push $REPO_URI

.stacks/ide/dictionary.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,13 @@ docgen
7676
domainkey
7777
dotenv
7878
dprint
79+
elasticloadbalancingv
80+
elbv
7981
encrypter
8082
entrypoints
8183
envrc
8284
execa
85+
Fargate
8386
filesize
8487
flareapp
8588
flowconfig

bun.lockb

66.1 KB
Binary file not shown.

config/docs.ts

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,54 @@ const sidebar = {
5151
],
5252
},
5353

54+
{
55+
text: 'Bootcamp',
56+
collapsible: true,
57+
items: [
58+
{ text: 'Introduction', link: '/bootcamp/intro' },
59+
{
60+
text: 'Let’s Build',
61+
collapsible: true,
62+
collapsed: true,
63+
items: [
64+
{ text: 'Build a Frontend', link: '/bootcamp/frontend' },
65+
{ text: 'Build an API', link: '/bootcamp/api' },
66+
{ text: 'Build a Documentation', link: '/bootcamp/docs' },
67+
{
68+
text: 'Build a Library',
69+
collapsible: true,
70+
collapsed: true,
71+
items: [
72+
{ text: 'Component Library', link: '/bootcamp/library/components' },
73+
{ text: 'Function Library', link: '/bootcamp/library/functions' },
74+
],
75+
},
76+
{ text: 'Build a CLI', link: '/bootcamp/cli' },
77+
{ text: 'Build a Desktop App', link: '/bootcamp/desktop' },
78+
{ text: 'Extend the Dashboard', link: '/bootcamp/dashboard' },
79+
{ text: 'Extend the Cloud', link: '/bootcamp/cloud' },
80+
],
81+
},
82+
{ text: 'IDE Setup', link: '/bootcamp/ide' },
83+
{ text: 'Semantic Commits', link: '/bootcamp/semantic-commits' },
84+
{ text: 'How to deploy?', link: '/bootcamp/deploy' },
85+
{ text: 'How to test?', link: '/bootcamp/testing' },
86+
{ text: 'How to publish?', link: '/bootcamp/publish' },
87+
{ text: 'How to “uninstall”?', link: '/bootcamp/uninstall' },
88+
{
89+
text: 'Tips',
90+
collapsible: true,
91+
collapsed: true,
92+
items: [
93+
{ text: 'Cost Optimization', link: '/bootcamp/tips/cost-optimization' },
94+
{ text: 'Jump Box', link: '/bootcamp/tips/jump-box' },
95+
{ text: 'Managing a team?', link: '/bootcamp/tips/team' },
96+
],
97+
},
98+
{ text: 'Conclusion', link: '/bootcamp/conclusion' },
99+
],
100+
},
101+
54102
{
55103
text: 'Digging Deeper',
56104
collapsible: true,
@@ -150,52 +198,6 @@ const sidebar = {
150198
link: '/terminology',
151199
},
152200
],
153-
154-
'/bootcamp/': [
155-
{
156-
text: 'Bootcamp',
157-
collapsible: true,
158-
items: [
159-
{ text: 'Introduction', link: '/bootcamp/intro' },
160-
{
161-
text: 'Let’s Build',
162-
items: [
163-
{ text: 'Build a Frontend', link: '/bootcamp/frontend' },
164-
{ text: 'Build an API', link: '/bootcamp/api' },
165-
{ text: 'Build a Documentation', link: '/bootcamp/docs' },
166-
{
167-
text: 'Build a Library',
168-
collapsible: true,
169-
collapsed: true,
170-
items: [
171-
{ text: 'Component Library', link: '/bootcamp/library/components' },
172-
{ text: 'Function Library', link: '/bootcamp/library/functions' },
173-
],
174-
},
175-
{ text: 'Build a CLI', link: '/bootcamp/cli' },
176-
{ text: 'Build a Desktop App', link: '/bootcamp/desktop' },
177-
{ text: 'Extend the Dashboard', link: '/bootcamp/dashboard' },
178-
{ text: 'Extend the Cloud', link: '/bootcamp/cloud' },
179-
],
180-
},
181-
{ text: 'Linting & Formatting', link: '/bootcamp/linting' },
182-
{ text: 'Semantic Commits', link: '/bootcamp/semantic-commits' },
183-
{ text: 'How to deploy?', link: '/bootcamp/deploy' },
184-
{ text: 'How to test?', link: '/bootcamp/testing' },
185-
{ text: 'How to publish?', link: '/bootcamp/publish' },
186-
{ text: 'How to “uninstall”?', link: '/bootcamp/uninstall' },
187-
{
188-
text: 'Tips',
189-
items: [
190-
{ text: 'Cost Optimization', link: '/bootcamp/tips/cost-optimization' },
191-
{ text: 'Jump Box', link: '/bootcamp/tips/jump-box' },
192-
{ text: 'Managing a team?', link: '/bootcamp/tips/team' },
193-
],
194-
},
195-
{ text: 'Conclusion', link: '/bootcamp/conclusion' },
196-
],
197-
},
198-
],
199201
}
200202

201203
/**

docs/bootcamp/ide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# How to setup your IDE
2+
3+
wip
4+
5+
## Linting & Formatting
6+
7+
wip

docs/guide/linting.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Linting & Formatting
2+
3+
wip

0 commit comments

Comments
 (0)