@stacksjs/ts-cloud-core 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
|
@@ -0,0 +1,3348 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplicationLoadBalancer,
|
|
3
|
+
AutoScalingGroup,
|
|
4
|
+
AutoScalingLaunchConfiguration,
|
|
5
|
+
AutoScalingScalingPolicy,
|
|
6
|
+
EC2Instance,
|
|
7
|
+
EC2SecurityGroup,
|
|
8
|
+
ECSCluster,
|
|
9
|
+
ECSService,
|
|
10
|
+
ECSTaskDefinition,
|
|
11
|
+
IAMRole,
|
|
12
|
+
LambdaFunction,
|
|
13
|
+
Listener,
|
|
14
|
+
TargetGroup,
|
|
15
|
+
} from '@stacksjs/ts-cloud-aws-types'
|
|
16
|
+
import type { EnvironmentType } from '@stacksjs/ts-cloud-types'
|
|
17
|
+
import { Fn } from '../intrinsic-functions'
|
|
18
|
+
import { generateLogicalId, generateResourceName } from '../resource-naming'
|
|
19
|
+
|
|
20
|
+
export interface ServerOptions {
|
|
21
|
+
slug: string
|
|
22
|
+
environment: EnvironmentType
|
|
23
|
+
instanceType?: string
|
|
24
|
+
imageId?: string
|
|
25
|
+
keyName?: string
|
|
26
|
+
securityGroupIds?: string[]
|
|
27
|
+
subnetId?: string
|
|
28
|
+
userData?: string
|
|
29
|
+
volumeSize?: number
|
|
30
|
+
volumeType?: 'gp2' | 'gp3' | 'io1' | 'io2'
|
|
31
|
+
encrypted?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SecurityGroupOptions {
|
|
35
|
+
slug: string
|
|
36
|
+
environment: EnvironmentType
|
|
37
|
+
vpcId?: string
|
|
38
|
+
description?: string
|
|
39
|
+
ingress?: SecurityGroupRule[]
|
|
40
|
+
egress?: SecurityGroupRule[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SecurityGroupRule {
|
|
44
|
+
protocol: string
|
|
45
|
+
fromPort?: number
|
|
46
|
+
toPort?: number
|
|
47
|
+
cidr?: string
|
|
48
|
+
sourceSecurityGroupId?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LoadBalancerOptions {
|
|
52
|
+
slug: string
|
|
53
|
+
environment: EnvironmentType
|
|
54
|
+
scheme?: 'internet-facing' | 'internal'
|
|
55
|
+
subnets: string[]
|
|
56
|
+
securityGroups?: string[]
|
|
57
|
+
type?: 'application' | 'network'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TargetGroupOptions {
|
|
61
|
+
slug: string
|
|
62
|
+
environment: EnvironmentType
|
|
63
|
+
port: number
|
|
64
|
+
protocol?: 'HTTP' | 'HTTPS' | 'TCP'
|
|
65
|
+
vpcId: string
|
|
66
|
+
targetType?: 'instance' | 'ip' | 'lambda'
|
|
67
|
+
healthCheckPath?: string
|
|
68
|
+
healthCheckInterval?: number
|
|
69
|
+
healthCheckTimeout?: number
|
|
70
|
+
healthyThreshold?: number
|
|
71
|
+
unhealthyThreshold?: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ListenerOptions {
|
|
75
|
+
port: number
|
|
76
|
+
protocol?: 'HTTP' | 'HTTPS'
|
|
77
|
+
certificateArn?: string
|
|
78
|
+
defaultTargetGroupArn: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface FargateServiceOptions {
|
|
82
|
+
slug: string
|
|
83
|
+
environment: EnvironmentType
|
|
84
|
+
image: string
|
|
85
|
+
cpu?: string
|
|
86
|
+
memory?: string
|
|
87
|
+
desiredCount?: number
|
|
88
|
+
containerPort?: number
|
|
89
|
+
environmentVariables?: Record<string, string>
|
|
90
|
+
secrets?: Array<{ name: string, valueFrom: string }>
|
|
91
|
+
healthCheck?: {
|
|
92
|
+
command: string[]
|
|
93
|
+
interval?: number
|
|
94
|
+
timeout?: number
|
|
95
|
+
retries?: number
|
|
96
|
+
}
|
|
97
|
+
logGroup?: string
|
|
98
|
+
subnets: string[]
|
|
99
|
+
securityGroups: string[]
|
|
100
|
+
targetGroupArn?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface LambdaFunctionOptions {
|
|
104
|
+
slug: string
|
|
105
|
+
environment: EnvironmentType
|
|
106
|
+
functionName?: string
|
|
107
|
+
runtime: string
|
|
108
|
+
handler: string
|
|
109
|
+
code: {
|
|
110
|
+
s3Bucket?: string
|
|
111
|
+
s3Key?: string
|
|
112
|
+
zipFile?: string
|
|
113
|
+
}
|
|
114
|
+
role?: string
|
|
115
|
+
timeout?: number
|
|
116
|
+
memorySize?: number
|
|
117
|
+
environmentVariables?: Record<string, string>
|
|
118
|
+
vpcConfig?: {
|
|
119
|
+
securityGroupIds: string[]
|
|
120
|
+
subnetIds: string[]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface LaunchConfigurationOptions {
|
|
125
|
+
slug: string
|
|
126
|
+
environment: EnvironmentType
|
|
127
|
+
imageId: string
|
|
128
|
+
instanceType: string
|
|
129
|
+
keyName?: string
|
|
130
|
+
securityGroups?: Array<string | { Ref: string }>
|
|
131
|
+
userData?: string
|
|
132
|
+
volumeSize?: number
|
|
133
|
+
volumeType?: 'gp2' | 'gp3' | 'io1' | 'io2'
|
|
134
|
+
encrypted?: boolean
|
|
135
|
+
iamInstanceProfile?: string | { Ref: string }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface AutoScalingGroupOptions {
|
|
139
|
+
slug: string
|
|
140
|
+
environment: EnvironmentType
|
|
141
|
+
launchConfigurationName: string | { Ref: string }
|
|
142
|
+
minSize: number
|
|
143
|
+
maxSize: number
|
|
144
|
+
desiredCapacity?: number
|
|
145
|
+
vpcZoneIdentifier?: string[] | { Ref: string }
|
|
146
|
+
targetGroupArns?: Array<string | { Ref: string }>
|
|
147
|
+
healthCheckType?: 'EC2' | 'ELB'
|
|
148
|
+
healthCheckGracePeriod?: number
|
|
149
|
+
cooldown?: number
|
|
150
|
+
tags?: Record<string, string>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ScalingPolicyOptions {
|
|
154
|
+
slug: string
|
|
155
|
+
environment: EnvironmentType
|
|
156
|
+
autoScalingGroupName: string | { Ref: string }
|
|
157
|
+
policyType?: 'TargetTrackingScaling' | 'StepScaling' | 'SimpleScaling'
|
|
158
|
+
targetValue?: number
|
|
159
|
+
predefinedMetricType?: 'ASGAverageCPUUtilization' | 'ASGAverageNetworkIn' | 'ASGAverageNetworkOut' | 'ALBRequestCountPerTarget'
|
|
160
|
+
scaleInCooldown?: number
|
|
161
|
+
scaleOutCooldown?: number
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Compute Module - EC2, ECS, Lambda Management
|
|
166
|
+
* Provides clean API for both server (Forge-style) and serverless (Vapor-style) deployments
|
|
167
|
+
*/
|
|
168
|
+
export class Compute {
|
|
169
|
+
/**
|
|
170
|
+
* Create an EC2 server instance (Server Mode - Forge-style)
|
|
171
|
+
*/
|
|
172
|
+
static createServer(options: ServerOptions): {
|
|
173
|
+
instance: EC2Instance
|
|
174
|
+
logicalId: string
|
|
175
|
+
} {
|
|
176
|
+
const {
|
|
177
|
+
slug,
|
|
178
|
+
environment,
|
|
179
|
+
instanceType = 't3.micro',
|
|
180
|
+
imageId = 'ami-0c55b159cbfafe1f0', // Amazon Linux 2023
|
|
181
|
+
keyName,
|
|
182
|
+
securityGroupIds,
|
|
183
|
+
subnetId,
|
|
184
|
+
userData,
|
|
185
|
+
volumeSize = 20,
|
|
186
|
+
volumeType = 'gp3',
|
|
187
|
+
encrypted = true,
|
|
188
|
+
} = options
|
|
189
|
+
|
|
190
|
+
const resourceName = generateResourceName({
|
|
191
|
+
slug,
|
|
192
|
+
environment,
|
|
193
|
+
resourceType: 'ec2',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const logicalId = generateLogicalId(resourceName)
|
|
197
|
+
|
|
198
|
+
const instance: EC2Instance = {
|
|
199
|
+
Type: 'AWS::EC2::Instance',
|
|
200
|
+
Properties: {
|
|
201
|
+
ImageId: imageId,
|
|
202
|
+
InstanceType: instanceType,
|
|
203
|
+
Tags: [
|
|
204
|
+
{ Key: 'Name', Value: resourceName },
|
|
205
|
+
{ Key: 'Environment', Value: environment },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (keyName) {
|
|
211
|
+
instance.Properties.KeyName = keyName
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (securityGroupIds) {
|
|
215
|
+
instance.Properties.SecurityGroupIds = securityGroupIds
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (subnetId) {
|
|
219
|
+
instance.Properties.SubnetId = subnetId
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (userData) {
|
|
223
|
+
// Base64 encode user data
|
|
224
|
+
instance.Properties.UserData = Fn.Base64(userData) as any
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Configure EBS volume
|
|
228
|
+
instance.Properties.BlockDeviceMappings = [
|
|
229
|
+
{
|
|
230
|
+
DeviceName: '/dev/xvda',
|
|
231
|
+
Ebs: {
|
|
232
|
+
VolumeSize: volumeSize,
|
|
233
|
+
VolumeType: volumeType,
|
|
234
|
+
Encrypted: encrypted,
|
|
235
|
+
DeleteOnTermination: true,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
return { instance, logicalId }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Create a security group
|
|
245
|
+
*/
|
|
246
|
+
static createSecurityGroup(options: SecurityGroupOptions): {
|
|
247
|
+
securityGroup: EC2SecurityGroup
|
|
248
|
+
logicalId: string
|
|
249
|
+
} {
|
|
250
|
+
const {
|
|
251
|
+
slug,
|
|
252
|
+
environment,
|
|
253
|
+
vpcId,
|
|
254
|
+
description,
|
|
255
|
+
ingress = [],
|
|
256
|
+
egress = [],
|
|
257
|
+
} = options
|
|
258
|
+
|
|
259
|
+
const resourceName = generateResourceName({
|
|
260
|
+
slug,
|
|
261
|
+
environment,
|
|
262
|
+
resourceType: 'sg',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const logicalId = generateLogicalId(resourceName)
|
|
266
|
+
|
|
267
|
+
const securityGroup: EC2SecurityGroup = {
|
|
268
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
269
|
+
Properties: {
|
|
270
|
+
GroupDescription: description || `Security group for ${slug} ${environment}`,
|
|
271
|
+
Tags: [
|
|
272
|
+
{ Key: 'Name', Value: resourceName },
|
|
273
|
+
{ Key: 'Environment', Value: environment },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (vpcId) {
|
|
279
|
+
securityGroup.Properties.VpcId = vpcId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (ingress.length > 0) {
|
|
283
|
+
securityGroup.Properties.SecurityGroupIngress = ingress.map(rule => ({
|
|
284
|
+
IpProtocol: rule.protocol,
|
|
285
|
+
FromPort: rule.fromPort,
|
|
286
|
+
ToPort: rule.toPort,
|
|
287
|
+
CidrIp: rule.cidr,
|
|
288
|
+
SourceSecurityGroupId: rule.sourceSecurityGroupId,
|
|
289
|
+
}))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (egress.length > 0) {
|
|
293
|
+
securityGroup.Properties.SecurityGroupEgress = egress.map(rule => ({
|
|
294
|
+
IpProtocol: rule.protocol,
|
|
295
|
+
FromPort: rule.fromPort,
|
|
296
|
+
ToPort: rule.toPort,
|
|
297
|
+
CidrIp: rule.cidr,
|
|
298
|
+
DestinationSecurityGroupId: rule.sourceSecurityGroupId,
|
|
299
|
+
}))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { securityGroup, logicalId }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create common security group rules for web servers
|
|
307
|
+
*/
|
|
308
|
+
static createWebServerSecurityGroup(
|
|
309
|
+
slug: string,
|
|
310
|
+
environment: EnvironmentType,
|
|
311
|
+
vpcId?: string,
|
|
312
|
+
): {
|
|
313
|
+
securityGroup: EC2SecurityGroup
|
|
314
|
+
logicalId: string
|
|
315
|
+
} {
|
|
316
|
+
return Compute.createSecurityGroup({
|
|
317
|
+
slug,
|
|
318
|
+
environment,
|
|
319
|
+
vpcId,
|
|
320
|
+
description: 'Security group for web servers - HTTP, HTTPS, SSH',
|
|
321
|
+
ingress: [
|
|
322
|
+
{ protocol: 'tcp', fromPort: 80, toPort: 80, cidr: '0.0.0.0/0' }, // HTTP
|
|
323
|
+
{ protocol: 'tcp', fromPort: 443, toPort: 443, cidr: '0.0.0.0/0' }, // HTTPS
|
|
324
|
+
{ protocol: 'tcp', fromPort: 22, toPort: 22, cidr: '0.0.0.0/0' }, // SSH (restrict in production)
|
|
325
|
+
],
|
|
326
|
+
egress: [
|
|
327
|
+
{ protocol: '-1', fromPort: 0, toPort: 0, cidr: '0.0.0.0/0' }, // All outbound
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create an Application Load Balancer
|
|
334
|
+
*/
|
|
335
|
+
static createLoadBalancer(options: LoadBalancerOptions): {
|
|
336
|
+
loadBalancer: ApplicationLoadBalancer
|
|
337
|
+
logicalId: string
|
|
338
|
+
} {
|
|
339
|
+
const {
|
|
340
|
+
slug,
|
|
341
|
+
environment,
|
|
342
|
+
scheme = 'internet-facing',
|
|
343
|
+
subnets,
|
|
344
|
+
securityGroups,
|
|
345
|
+
type = 'application',
|
|
346
|
+
} = options
|
|
347
|
+
|
|
348
|
+
const resourceName = generateResourceName({
|
|
349
|
+
slug,
|
|
350
|
+
environment,
|
|
351
|
+
resourceType: 'alb',
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const logicalId = generateLogicalId(resourceName)
|
|
355
|
+
|
|
356
|
+
const loadBalancer: ApplicationLoadBalancer = {
|
|
357
|
+
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer',
|
|
358
|
+
Properties: {
|
|
359
|
+
Name: resourceName,
|
|
360
|
+
Scheme: scheme,
|
|
361
|
+
Type: type,
|
|
362
|
+
Subnets: subnets,
|
|
363
|
+
Tags: [
|
|
364
|
+
{ Key: 'Name', Value: resourceName },
|
|
365
|
+
{ Key: 'Environment', Value: environment },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (securityGroups) {
|
|
371
|
+
loadBalancer.Properties.SecurityGroups = securityGroups
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { loadBalancer, logicalId }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create a target group
|
|
379
|
+
*/
|
|
380
|
+
static createTargetGroup(options: TargetGroupOptions): {
|
|
381
|
+
targetGroup: TargetGroup
|
|
382
|
+
logicalId: string
|
|
383
|
+
} {
|
|
384
|
+
const {
|
|
385
|
+
slug,
|
|
386
|
+
environment,
|
|
387
|
+
port,
|
|
388
|
+
protocol = 'HTTP',
|
|
389
|
+
vpcId,
|
|
390
|
+
targetType = 'ip',
|
|
391
|
+
healthCheckPath = '/',
|
|
392
|
+
healthCheckInterval = 30,
|
|
393
|
+
healthCheckTimeout = 5,
|
|
394
|
+
healthyThreshold = 2,
|
|
395
|
+
unhealthyThreshold = 3,
|
|
396
|
+
} = options
|
|
397
|
+
|
|
398
|
+
const resourceName = generateResourceName({
|
|
399
|
+
slug,
|
|
400
|
+
environment,
|
|
401
|
+
resourceType: 'tg',
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const logicalId = generateLogicalId(resourceName)
|
|
405
|
+
|
|
406
|
+
const targetGroup: TargetGroup = {
|
|
407
|
+
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup',
|
|
408
|
+
Properties: {
|
|
409
|
+
Name: resourceName,
|
|
410
|
+
Port: port,
|
|
411
|
+
Protocol: protocol,
|
|
412
|
+
VpcId: vpcId,
|
|
413
|
+
TargetType: targetType,
|
|
414
|
+
HealthCheckEnabled: true,
|
|
415
|
+
HealthCheckProtocol: protocol,
|
|
416
|
+
HealthCheckPath: healthCheckPath,
|
|
417
|
+
HealthCheckIntervalSeconds: healthCheckInterval,
|
|
418
|
+
HealthCheckTimeoutSeconds: healthCheckTimeout,
|
|
419
|
+
HealthyThresholdCount: healthyThreshold,
|
|
420
|
+
UnhealthyThresholdCount: unhealthyThreshold,
|
|
421
|
+
Tags: [
|
|
422
|
+
{ Key: 'Name', Value: resourceName },
|
|
423
|
+
{ Key: 'Environment', Value: environment },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { targetGroup, logicalId }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create an ALB listener
|
|
433
|
+
*/
|
|
434
|
+
static createListener(
|
|
435
|
+
loadBalancerLogicalId: string,
|
|
436
|
+
options: ListenerOptions,
|
|
437
|
+
): {
|
|
438
|
+
listener: Listener
|
|
439
|
+
logicalId: string
|
|
440
|
+
} {
|
|
441
|
+
const {
|
|
442
|
+
port,
|
|
443
|
+
protocol = 'HTTP',
|
|
444
|
+
certificateArn,
|
|
445
|
+
defaultTargetGroupArn,
|
|
446
|
+
} = options
|
|
447
|
+
|
|
448
|
+
const logicalId = generateLogicalId(`listener-${loadBalancerLogicalId}-${port}`)
|
|
449
|
+
|
|
450
|
+
const listener: Listener = {
|
|
451
|
+
Type: 'AWS::ElasticLoadBalancingV2::Listener',
|
|
452
|
+
Properties: {
|
|
453
|
+
LoadBalancerArn: Fn.Ref(loadBalancerLogicalId),
|
|
454
|
+
Port: port,
|
|
455
|
+
Protocol: protocol,
|
|
456
|
+
DefaultActions: [
|
|
457
|
+
{
|
|
458
|
+
Type: 'forward',
|
|
459
|
+
TargetGroupArn: defaultTargetGroupArn,
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (protocol === 'HTTPS' && certificateArn) {
|
|
466
|
+
listener.Properties.Certificates = [{ CertificateArn: certificateArn }]
|
|
467
|
+
listener.Properties.SslPolicy = 'ELBSecurityPolicy-TLS13-1-2-2021-06'
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return { listener, logicalId }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Create ECS cluster for Fargate (Serverless Mode - Vapor-style)
|
|
475
|
+
*/
|
|
476
|
+
static createEcsCluster(
|
|
477
|
+
slug: string,
|
|
478
|
+
environment: EnvironmentType,
|
|
479
|
+
): {
|
|
480
|
+
cluster: ECSCluster
|
|
481
|
+
logicalId: string
|
|
482
|
+
} {
|
|
483
|
+
const resourceName = generateResourceName({
|
|
484
|
+
slug,
|
|
485
|
+
environment,
|
|
486
|
+
resourceType: 'ecs-cluster',
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const logicalId = generateLogicalId(resourceName)
|
|
490
|
+
|
|
491
|
+
const cluster: ECSCluster = {
|
|
492
|
+
Type: 'AWS::ECS::Cluster',
|
|
493
|
+
Properties: {
|
|
494
|
+
ClusterName: resourceName,
|
|
495
|
+
Tags: [
|
|
496
|
+
{ Key: 'Name', Value: resourceName },
|
|
497
|
+
{ Key: 'Environment', Value: environment },
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { cluster, logicalId }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Create ECS Fargate task definition and service
|
|
507
|
+
*/
|
|
508
|
+
static createFargateService(options: FargateServiceOptions): {
|
|
509
|
+
cluster: ECSCluster
|
|
510
|
+
taskDefinition: ECSTaskDefinition
|
|
511
|
+
service: ECSService
|
|
512
|
+
taskRole: IAMRole
|
|
513
|
+
executionRole: IAMRole
|
|
514
|
+
clusterLogicalId: string
|
|
515
|
+
taskDefinitionLogicalId: string
|
|
516
|
+
serviceLogicalId: string
|
|
517
|
+
taskRoleLogicalId: string
|
|
518
|
+
executionRoleLogicalId: string
|
|
519
|
+
} {
|
|
520
|
+
const {
|
|
521
|
+
slug,
|
|
522
|
+
environment,
|
|
523
|
+
image,
|
|
524
|
+
cpu = '256',
|
|
525
|
+
memory = '512',
|
|
526
|
+
desiredCount = 1,
|
|
527
|
+
containerPort = 8080,
|
|
528
|
+
environmentVariables = {},
|
|
529
|
+
secrets = [],
|
|
530
|
+
healthCheck,
|
|
531
|
+
logGroup,
|
|
532
|
+
subnets,
|
|
533
|
+
securityGroups,
|
|
534
|
+
targetGroupArn,
|
|
535
|
+
} = options
|
|
536
|
+
|
|
537
|
+
// Create ECS Cluster
|
|
538
|
+
const { cluster, logicalId: clusterLogicalId } = Compute.createEcsCluster(slug, environment)
|
|
539
|
+
|
|
540
|
+
const resourceName = generateResourceName({
|
|
541
|
+
slug,
|
|
542
|
+
environment,
|
|
543
|
+
resourceType: 'fargate',
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// Create Task Execution Role (needed for pulling images, logging, etc.)
|
|
547
|
+
const executionRoleLogicalId = generateLogicalId(`${resourceName}-execution-role`)
|
|
548
|
+
const executionRole: IAMRole = {
|
|
549
|
+
Type: 'AWS::IAM::Role',
|
|
550
|
+
Properties: {
|
|
551
|
+
AssumeRolePolicyDocument: {
|
|
552
|
+
Version: '2012-10-17',
|
|
553
|
+
Statement: [
|
|
554
|
+
{
|
|
555
|
+
Effect: 'Allow',
|
|
556
|
+
Principal: {
|
|
557
|
+
Service: 'ecs-tasks.amazonaws.com',
|
|
558
|
+
},
|
|
559
|
+
Action: 'sts:AssumeRole',
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
ManagedPolicyArns: [
|
|
564
|
+
'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Create Task Role (for application permissions)
|
|
570
|
+
const taskRoleLogicalId = generateLogicalId(`${resourceName}-task-role`)
|
|
571
|
+
const taskRole: IAMRole = {
|
|
572
|
+
Type: 'AWS::IAM::Role',
|
|
573
|
+
Properties: {
|
|
574
|
+
AssumeRolePolicyDocument: {
|
|
575
|
+
Version: '2012-10-17',
|
|
576
|
+
Statement: [
|
|
577
|
+
{
|
|
578
|
+
Effect: 'Allow',
|
|
579
|
+
Principal: {
|
|
580
|
+
Service: 'ecs-tasks.amazonaws.com',
|
|
581
|
+
},
|
|
582
|
+
Action: 'sts:AssumeRole',
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Create Task Definition
|
|
590
|
+
const taskDefinitionLogicalId = generateLogicalId(`${resourceName}-task`)
|
|
591
|
+
const taskDefinition: ECSTaskDefinition = {
|
|
592
|
+
Type: 'AWS::ECS::TaskDefinition',
|
|
593
|
+
Properties: {
|
|
594
|
+
Family: resourceName,
|
|
595
|
+
TaskRoleArn: Fn.GetAtt(taskRoleLogicalId, 'Arn') as any,
|
|
596
|
+
ExecutionRoleArn: Fn.GetAtt(executionRoleLogicalId, 'Arn') as any,
|
|
597
|
+
NetworkMode: 'awsvpc',
|
|
598
|
+
RequiresCompatibilities: ['FARGATE'],
|
|
599
|
+
Cpu: cpu,
|
|
600
|
+
Memory: memory,
|
|
601
|
+
ContainerDefinitions: [
|
|
602
|
+
{
|
|
603
|
+
Name: slug,
|
|
604
|
+
Image: image,
|
|
605
|
+
Essential: true,
|
|
606
|
+
PortMappings: [
|
|
607
|
+
{
|
|
608
|
+
ContainerPort: containerPort,
|
|
609
|
+
Protocol: 'tcp',
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
Environment: Object.entries(environmentVariables).map(([Name, Value]) => ({
|
|
613
|
+
Name,
|
|
614
|
+
Value,
|
|
615
|
+
})),
|
|
616
|
+
Secrets: secrets.map(s => ({
|
|
617
|
+
Name: s.name,
|
|
618
|
+
ValueFrom: s.valueFrom,
|
|
619
|
+
})),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
Tags: [
|
|
623
|
+
{ Key: 'Name', Value: resourceName },
|
|
624
|
+
{ Key: 'Environment', Value: environment },
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Add health check if provided
|
|
630
|
+
if (healthCheck) {
|
|
631
|
+
taskDefinition.Properties.ContainerDefinitions[0].HealthCheck = {
|
|
632
|
+
Command: healthCheck.command,
|
|
633
|
+
Interval: healthCheck.interval || 30,
|
|
634
|
+
Timeout: healthCheck.timeout || 5,
|
|
635
|
+
Retries: healthCheck.retries || 3,
|
|
636
|
+
StartPeriod: 60,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Add logging configuration
|
|
641
|
+
if (logGroup) {
|
|
642
|
+
taskDefinition.Properties.ContainerDefinitions[0].LogConfiguration = {
|
|
643
|
+
LogDriver: 'awslogs',
|
|
644
|
+
Options: {
|
|
645
|
+
'awslogs-group': logGroup,
|
|
646
|
+
'awslogs-region': Fn.Ref('AWS::Region') as any,
|
|
647
|
+
'awslogs-stream-prefix': slug,
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Create ECS Service
|
|
653
|
+
const serviceLogicalId = generateLogicalId(`${resourceName}-service`)
|
|
654
|
+
const service: ECSService = {
|
|
655
|
+
Type: 'AWS::ECS::Service',
|
|
656
|
+
Properties: {
|
|
657
|
+
ServiceName: resourceName,
|
|
658
|
+
Cluster: Fn.Ref(clusterLogicalId),
|
|
659
|
+
TaskDefinition: Fn.Ref(taskDefinitionLogicalId),
|
|
660
|
+
DesiredCount: desiredCount,
|
|
661
|
+
LaunchType: 'FARGATE',
|
|
662
|
+
NetworkConfiguration: {
|
|
663
|
+
AwsvpcConfiguration: {
|
|
664
|
+
Subnets: subnets,
|
|
665
|
+
SecurityGroups: securityGroups,
|
|
666
|
+
AssignPublicIp: 'ENABLED',
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
Tags: [
|
|
670
|
+
{ Key: 'Name', Value: resourceName },
|
|
671
|
+
{ Key: 'Environment', Value: environment },
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Add load balancer integration if target group provided
|
|
677
|
+
if (targetGroupArn) {
|
|
678
|
+
service.Properties.LoadBalancers = [
|
|
679
|
+
{
|
|
680
|
+
TargetGroupArn: targetGroupArn,
|
|
681
|
+
ContainerName: slug,
|
|
682
|
+
ContainerPort: containerPort,
|
|
683
|
+
},
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
cluster,
|
|
689
|
+
taskDefinition,
|
|
690
|
+
service,
|
|
691
|
+
taskRole,
|
|
692
|
+
executionRole,
|
|
693
|
+
clusterLogicalId,
|
|
694
|
+
taskDefinitionLogicalId,
|
|
695
|
+
serviceLogicalId,
|
|
696
|
+
taskRoleLogicalId,
|
|
697
|
+
executionRoleLogicalId,
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Create a Lambda function
|
|
703
|
+
*/
|
|
704
|
+
static createLambdaFunction(options: LambdaFunctionOptions): {
|
|
705
|
+
lambdaFunction: LambdaFunction
|
|
706
|
+
role: IAMRole
|
|
707
|
+
logicalId: string
|
|
708
|
+
roleLogicalId: string
|
|
709
|
+
} {
|
|
710
|
+
const {
|
|
711
|
+
slug,
|
|
712
|
+
environment,
|
|
713
|
+
runtime,
|
|
714
|
+
handler,
|
|
715
|
+
code,
|
|
716
|
+
timeout = 30,
|
|
717
|
+
memorySize = 128,
|
|
718
|
+
environmentVariables = {},
|
|
719
|
+
vpcConfig,
|
|
720
|
+
} = options
|
|
721
|
+
|
|
722
|
+
const resourceName = generateResourceName({
|
|
723
|
+
slug,
|
|
724
|
+
environment,
|
|
725
|
+
resourceType: 'lambda',
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
const logicalId = generateLogicalId(resourceName)
|
|
729
|
+
|
|
730
|
+
// Create Lambda execution role
|
|
731
|
+
const roleLogicalId = generateLogicalId(`${resourceName}-role`)
|
|
732
|
+
const role: IAMRole = {
|
|
733
|
+
Type: 'AWS::IAM::Role',
|
|
734
|
+
Properties: {
|
|
735
|
+
AssumeRolePolicyDocument: {
|
|
736
|
+
Version: '2012-10-17',
|
|
737
|
+
Statement: [
|
|
738
|
+
{
|
|
739
|
+
Effect: 'Allow',
|
|
740
|
+
Principal: {
|
|
741
|
+
Service: 'lambda.amazonaws.com',
|
|
742
|
+
},
|
|
743
|
+
Action: 'sts:AssumeRole',
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
ManagedPolicyArns: [
|
|
748
|
+
'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Add VPC execution role if VPC config provided
|
|
754
|
+
if (vpcConfig) {
|
|
755
|
+
role.Properties.ManagedPolicyArns!.push(
|
|
756
|
+
'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const lambdaFunction: LambdaFunction = {
|
|
761
|
+
Type: 'AWS::Lambda::Function',
|
|
762
|
+
Properties: {
|
|
763
|
+
FunctionName: resourceName,
|
|
764
|
+
Runtime: runtime,
|
|
765
|
+
Role: Fn.GetAtt(roleLogicalId, 'Arn') as any,
|
|
766
|
+
Handler: handler,
|
|
767
|
+
Code: {
|
|
768
|
+
...(code.s3Bucket && { S3Bucket: code.s3Bucket }),
|
|
769
|
+
...(code.s3Key && { S3Key: code.s3Key }),
|
|
770
|
+
...(code.zipFile && { ZipFile: code.zipFile }),
|
|
771
|
+
},
|
|
772
|
+
Timeout: timeout,
|
|
773
|
+
MemorySize: memorySize,
|
|
774
|
+
Tags: [
|
|
775
|
+
{ Key: 'Name', Value: resourceName },
|
|
776
|
+
{ Key: 'Environment', Value: environment },
|
|
777
|
+
],
|
|
778
|
+
},
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (Object.keys(environmentVariables).length > 0) {
|
|
782
|
+
lambdaFunction.Properties.Environment = {
|
|
783
|
+
Variables: environmentVariables,
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (vpcConfig) {
|
|
788
|
+
lambdaFunction.Properties.VpcConfig = {
|
|
789
|
+
SecurityGroupIds: vpcConfig.securityGroupIds,
|
|
790
|
+
SubnetIds: vpcConfig.subnetIds,
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return { lambdaFunction, role, logicalId, roleLogicalId }
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Generate Node.js server user data script
|
|
799
|
+
*/
|
|
800
|
+
static generateNodeServerUserData(options: {
|
|
801
|
+
nodeVersion?: string
|
|
802
|
+
appRepo?: string
|
|
803
|
+
environment?: Record<string, string>
|
|
804
|
+
} = {}): string {
|
|
805
|
+
const { nodeVersion = '20', appRepo, environment = {} } = options
|
|
806
|
+
|
|
807
|
+
const envVars = Object.entries(environment)
|
|
808
|
+
.map(([key, value]) => `echo "export ${key}='${value}'" >> /etc/environment`)
|
|
809
|
+
.join('\n')
|
|
810
|
+
|
|
811
|
+
return `#!/bin/bash
|
|
812
|
+
# Update system
|
|
813
|
+
yum update -y
|
|
814
|
+
|
|
815
|
+
# Install Node.js ${nodeVersion}
|
|
816
|
+
curl -fsSL https://rpm.nodesource.com/setup_${nodeVersion}.x | bash -
|
|
817
|
+
yum install -y nodejs
|
|
818
|
+
|
|
819
|
+
# Install PM2 for process management
|
|
820
|
+
npm install -g pm2
|
|
821
|
+
|
|
822
|
+
# Install Caddy for reverse proxy and automatic HTTPS
|
|
823
|
+
yum install -y yum-plugin-copr
|
|
824
|
+
yum copr enable -y @caddy/caddy
|
|
825
|
+
yum install -y caddy
|
|
826
|
+
|
|
827
|
+
# Set environment variables
|
|
828
|
+
${envVars}
|
|
829
|
+
|
|
830
|
+
# Clone application (if repo provided)
|
|
831
|
+
${appRepo ? `
|
|
832
|
+
cd /var/www
|
|
833
|
+
git clone ${appRepo} app
|
|
834
|
+
cd app
|
|
835
|
+
npm install
|
|
836
|
+
pm2 start npm --name 'app' -- start
|
|
837
|
+
pm2 save
|
|
838
|
+
pm2 startup systemd -u ec2-user --hp /home/ec2-user
|
|
839
|
+
` : '# No repository specified'}
|
|
840
|
+
|
|
841
|
+
# Configure Caddy
|
|
842
|
+
cat > /etc/caddy/Caddyfile <<'EOF'
|
|
843
|
+
:80 {
|
|
844
|
+
reverse_proxy localhost:3000
|
|
845
|
+
}
|
|
846
|
+
EOF
|
|
847
|
+
|
|
848
|
+
# Start Caddy
|
|
849
|
+
systemctl enable caddy
|
|
850
|
+
systemctl start caddy
|
|
851
|
+
|
|
852
|
+
echo "Server setup complete!"
|
|
853
|
+
`
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Generate Bun server user data script
|
|
858
|
+
*/
|
|
859
|
+
static generateBunServerUserData(options: {
|
|
860
|
+
appRepo?: string
|
|
861
|
+
environment?: Record<string, string>
|
|
862
|
+
} = {}): string {
|
|
863
|
+
const { appRepo, environment = {} } = options
|
|
864
|
+
|
|
865
|
+
const envVars = Object.entries(environment)
|
|
866
|
+
.map(([key, value]) => `echo "export ${key}='${value}'" >> /etc/environment`)
|
|
867
|
+
.join('\n')
|
|
868
|
+
|
|
869
|
+
return `#!/bin/bash
|
|
870
|
+
# Update system
|
|
871
|
+
yum update -y
|
|
872
|
+
|
|
873
|
+
# Install Bun
|
|
874
|
+
curl -fsSL https://bun.sh/install | bash
|
|
875
|
+
echo 'export BUN_INSTALL="/root/.bun"' >> /root/.bashrc
|
|
876
|
+
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /root/.bashrc
|
|
877
|
+
source /root/.bashrc
|
|
878
|
+
|
|
879
|
+
# Install Caddy
|
|
880
|
+
yum install -y yum-plugin-copr
|
|
881
|
+
yum copr enable -y @caddy/caddy
|
|
882
|
+
yum install -y caddy
|
|
883
|
+
|
|
884
|
+
# Set environment variables
|
|
885
|
+
${envVars}
|
|
886
|
+
|
|
887
|
+
# Clone application (if repo provided)
|
|
888
|
+
${appRepo ? `
|
|
889
|
+
cd /var/www
|
|
890
|
+
git clone ${appRepo} app
|
|
891
|
+
cd app
|
|
892
|
+
bun install
|
|
893
|
+
|
|
894
|
+
# Create systemd service
|
|
895
|
+
cat > /etc/systemd/system/app.service <<'SERVICE'
|
|
896
|
+
[Unit]
|
|
897
|
+
Description=Bun Application
|
|
898
|
+
After=network.target
|
|
899
|
+
|
|
900
|
+
[Service]
|
|
901
|
+
Type=simple
|
|
902
|
+
User=root
|
|
903
|
+
WorkingDirectory=/var/www/app
|
|
904
|
+
ExecStart=/root/.bun/bin/bun run start
|
|
905
|
+
Restart=always
|
|
906
|
+
|
|
907
|
+
[Install]
|
|
908
|
+
WantedBy=multi-user.target
|
|
909
|
+
SERVICE
|
|
910
|
+
|
|
911
|
+
systemctl enable app
|
|
912
|
+
systemctl start app
|
|
913
|
+
` : '# No repository specified'}
|
|
914
|
+
|
|
915
|
+
# Configure Caddy
|
|
916
|
+
cat > /etc/caddy/Caddyfile <<'EOF'
|
|
917
|
+
:80 {
|
|
918
|
+
reverse_proxy localhost:3000
|
|
919
|
+
}
|
|
920
|
+
EOF
|
|
921
|
+
|
|
922
|
+
systemctl enable caddy
|
|
923
|
+
systemctl start caddy
|
|
924
|
+
|
|
925
|
+
echo "Bun server setup complete!"
|
|
926
|
+
`
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Create a Launch Configuration for Auto Scaling
|
|
931
|
+
*/
|
|
932
|
+
static createLaunchConfiguration(options: LaunchConfigurationOptions): {
|
|
933
|
+
launchConfiguration: AutoScalingLaunchConfiguration
|
|
934
|
+
logicalId: string
|
|
935
|
+
} {
|
|
936
|
+
const {
|
|
937
|
+
slug,
|
|
938
|
+
environment,
|
|
939
|
+
imageId,
|
|
940
|
+
instanceType,
|
|
941
|
+
keyName,
|
|
942
|
+
securityGroups,
|
|
943
|
+
userData,
|
|
944
|
+
volumeSize = 20,
|
|
945
|
+
volumeType = 'gp3',
|
|
946
|
+
encrypted = true,
|
|
947
|
+
iamInstanceProfile,
|
|
948
|
+
} = options
|
|
949
|
+
|
|
950
|
+
const resourceName = generateResourceName({
|
|
951
|
+
slug,
|
|
952
|
+
environment,
|
|
953
|
+
resourceType: 'launch-config',
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
const logicalId = generateLogicalId(resourceName)
|
|
957
|
+
|
|
958
|
+
const launchConfiguration: AutoScalingLaunchConfiguration = {
|
|
959
|
+
Type: 'AWS::AutoScaling::LaunchConfiguration',
|
|
960
|
+
Properties: {
|
|
961
|
+
ImageId: imageId,
|
|
962
|
+
InstanceType: instanceType,
|
|
963
|
+
BlockDeviceMappings: [
|
|
964
|
+
{
|
|
965
|
+
DeviceName: '/dev/xvda',
|
|
966
|
+
Ebs: {
|
|
967
|
+
VolumeSize: volumeSize,
|
|
968
|
+
VolumeType: volumeType,
|
|
969
|
+
Encrypted: encrypted,
|
|
970
|
+
DeleteOnTermination: true,
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
},
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (keyName) {
|
|
978
|
+
launchConfiguration.Properties.KeyName = keyName
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (securityGroups) {
|
|
982
|
+
launchConfiguration.Properties.SecurityGroups = securityGroups
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (userData) {
|
|
986
|
+
launchConfiguration.Properties.UserData = Fn.Base64(userData)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (iamInstanceProfile) {
|
|
990
|
+
launchConfiguration.Properties.IamInstanceProfile = iamInstanceProfile
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return { launchConfiguration, logicalId }
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Create an Auto Scaling Group
|
|
998
|
+
*/
|
|
999
|
+
static createAutoScalingGroup(options: AutoScalingGroupOptions): {
|
|
1000
|
+
autoScalingGroup: AutoScalingGroup
|
|
1001
|
+
logicalId: string
|
|
1002
|
+
} {
|
|
1003
|
+
const {
|
|
1004
|
+
slug,
|
|
1005
|
+
environment,
|
|
1006
|
+
launchConfigurationName,
|
|
1007
|
+
minSize,
|
|
1008
|
+
maxSize,
|
|
1009
|
+
desiredCapacity,
|
|
1010
|
+
vpcZoneIdentifier,
|
|
1011
|
+
targetGroupArns,
|
|
1012
|
+
healthCheckType = 'EC2',
|
|
1013
|
+
healthCheckGracePeriod = 300,
|
|
1014
|
+
cooldown = 300,
|
|
1015
|
+
tags = {},
|
|
1016
|
+
} = options
|
|
1017
|
+
|
|
1018
|
+
const resourceName = generateResourceName({
|
|
1019
|
+
slug,
|
|
1020
|
+
environment,
|
|
1021
|
+
resourceType: 'asg',
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
const logicalId = generateLogicalId(resourceName)
|
|
1025
|
+
|
|
1026
|
+
const autoScalingGroup: AutoScalingGroup = {
|
|
1027
|
+
Type: 'AWS::AutoScaling::AutoScalingGroup',
|
|
1028
|
+
Properties: {
|
|
1029
|
+
AutoScalingGroupName: resourceName,
|
|
1030
|
+
LaunchConfigurationName: launchConfigurationName,
|
|
1031
|
+
MinSize: minSize,
|
|
1032
|
+
MaxSize: maxSize,
|
|
1033
|
+
HealthCheckType: healthCheckType,
|
|
1034
|
+
HealthCheckGracePeriod: healthCheckGracePeriod,
|
|
1035
|
+
Cooldown: cooldown,
|
|
1036
|
+
Tags: [
|
|
1037
|
+
{ Key: 'Name', Value: resourceName, PropagateAtLaunch: true },
|
|
1038
|
+
{ Key: 'Environment', Value: environment, PropagateAtLaunch: true },
|
|
1039
|
+
...Object.entries(tags).map(([key, value]) => ({
|
|
1040
|
+
Key: key,
|
|
1041
|
+
Value: value,
|
|
1042
|
+
PropagateAtLaunch: true,
|
|
1043
|
+
})),
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (desiredCapacity !== undefined) {
|
|
1049
|
+
autoScalingGroup.Properties.DesiredCapacity = desiredCapacity
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (vpcZoneIdentifier) {
|
|
1053
|
+
autoScalingGroup.Properties.VPCZoneIdentifier = vpcZoneIdentifier
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (targetGroupArns) {
|
|
1057
|
+
autoScalingGroup.Properties.TargetGroupARNs = targetGroupArns
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Add rolling update policy for safer deployments
|
|
1061
|
+
autoScalingGroup.UpdatePolicy = {
|
|
1062
|
+
AutoScalingRollingUpdate: {
|
|
1063
|
+
MaxBatchSize: 1,
|
|
1064
|
+
MinInstancesInService: Math.max(0, minSize - 1),
|
|
1065
|
+
PauseTime: 'PT5M',
|
|
1066
|
+
WaitOnResourceSignals: false,
|
|
1067
|
+
},
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return { autoScalingGroup, logicalId }
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Create a Target Tracking Scaling Policy (CPU-based by default)
|
|
1075
|
+
*/
|
|
1076
|
+
static createScalingPolicy(options: ScalingPolicyOptions): {
|
|
1077
|
+
scalingPolicy: AutoScalingScalingPolicy
|
|
1078
|
+
logicalId: string
|
|
1079
|
+
} {
|
|
1080
|
+
const {
|
|
1081
|
+
slug,
|
|
1082
|
+
environment,
|
|
1083
|
+
autoScalingGroupName,
|
|
1084
|
+
policyType = 'TargetTrackingScaling',
|
|
1085
|
+
targetValue = 70, // 70% CPU by default
|
|
1086
|
+
predefinedMetricType = 'ASGAverageCPUUtilization',
|
|
1087
|
+
} = options
|
|
1088
|
+
|
|
1089
|
+
const resourceName = generateResourceName({
|
|
1090
|
+
slug,
|
|
1091
|
+
environment,
|
|
1092
|
+
resourceType: 'scaling-policy',
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
const logicalId = generateLogicalId(resourceName)
|
|
1096
|
+
|
|
1097
|
+
const scalingPolicy: AutoScalingScalingPolicy = {
|
|
1098
|
+
Type: 'AWS::AutoScaling::ScalingPolicy',
|
|
1099
|
+
Properties: {
|
|
1100
|
+
AutoScalingGroupName: autoScalingGroupName,
|
|
1101
|
+
PolicyType: policyType,
|
|
1102
|
+
},
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (policyType === 'TargetTrackingScaling') {
|
|
1106
|
+
scalingPolicy.Properties.TargetTrackingConfiguration = {
|
|
1107
|
+
PredefinedMetricSpecification: {
|
|
1108
|
+
PredefinedMetricType: predefinedMetricType,
|
|
1109
|
+
},
|
|
1110
|
+
TargetValue: targetValue,
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return { scalingPolicy, logicalId }
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Common Auto Scaling configurations
|
|
1119
|
+
*/
|
|
1120
|
+
static readonly AutoScaling = {
|
|
1121
|
+
/**
|
|
1122
|
+
* Small web server auto scaling (2-4 instances)
|
|
1123
|
+
*/
|
|
1124
|
+
smallWebServer: (
|
|
1125
|
+
slug: string,
|
|
1126
|
+
environment: EnvironmentType,
|
|
1127
|
+
launchConfigRef: string | { Ref: string },
|
|
1128
|
+
subnetIds: string[],
|
|
1129
|
+
targetGroupArns?: Array<string | { Ref: string }>,
|
|
1130
|
+
): { autoScalingGroup: AutoScalingGroup; logicalId: string } => {
|
|
1131
|
+
return Compute.createAutoScalingGroup({
|
|
1132
|
+
slug,
|
|
1133
|
+
environment,
|
|
1134
|
+
launchConfigurationName: launchConfigRef,
|
|
1135
|
+
minSize: 2,
|
|
1136
|
+
maxSize: 4,
|
|
1137
|
+
desiredCapacity: 2,
|
|
1138
|
+
vpcZoneIdentifier: subnetIds,
|
|
1139
|
+
targetGroupArns,
|
|
1140
|
+
healthCheckType: targetGroupArns ? 'ELB' : 'EC2',
|
|
1141
|
+
healthCheckGracePeriod: 300,
|
|
1142
|
+
})
|
|
1143
|
+
},
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Medium web server auto scaling (3-10 instances)
|
|
1147
|
+
*/
|
|
1148
|
+
mediumWebServer: (
|
|
1149
|
+
slug: string,
|
|
1150
|
+
environment: EnvironmentType,
|
|
1151
|
+
launchConfigRef: string | { Ref: string },
|
|
1152
|
+
subnetIds: string[],
|
|
1153
|
+
targetGroupArns?: Array<string | { Ref: string }>,
|
|
1154
|
+
): { autoScalingGroup: AutoScalingGroup; logicalId: string } => {
|
|
1155
|
+
return Compute.createAutoScalingGroup({
|
|
1156
|
+
slug,
|
|
1157
|
+
environment,
|
|
1158
|
+
launchConfigurationName: launchConfigRef,
|
|
1159
|
+
minSize: 3,
|
|
1160
|
+
maxSize: 10,
|
|
1161
|
+
desiredCapacity: 3,
|
|
1162
|
+
vpcZoneIdentifier: subnetIds,
|
|
1163
|
+
targetGroupArns,
|
|
1164
|
+
healthCheckType: targetGroupArns ? 'ELB' : 'EC2',
|
|
1165
|
+
healthCheckGracePeriod: 300,
|
|
1166
|
+
})
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Large web server auto scaling (5-20 instances)
|
|
1171
|
+
*/
|
|
1172
|
+
largeWebServer: (
|
|
1173
|
+
slug: string,
|
|
1174
|
+
environment: EnvironmentType,
|
|
1175
|
+
launchConfigRef: string | { Ref: string },
|
|
1176
|
+
subnetIds: string[],
|
|
1177
|
+
targetGroupArns?: Array<string | { Ref: string }>,
|
|
1178
|
+
): { autoScalingGroup: AutoScalingGroup; logicalId: string } => {
|
|
1179
|
+
return Compute.createAutoScalingGroup({
|
|
1180
|
+
slug,
|
|
1181
|
+
environment,
|
|
1182
|
+
launchConfigurationName: launchConfigRef,
|
|
1183
|
+
minSize: 5,
|
|
1184
|
+
maxSize: 20,
|
|
1185
|
+
desiredCapacity: 5,
|
|
1186
|
+
vpcZoneIdentifier: subnetIds,
|
|
1187
|
+
targetGroupArns,
|
|
1188
|
+
healthCheckType: targetGroupArns ? 'ELB' : 'EC2',
|
|
1189
|
+
healthCheckGracePeriod: 300,
|
|
1190
|
+
})
|
|
1191
|
+
},
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* CPU-based scaling policy (default 70%)
|
|
1195
|
+
*/
|
|
1196
|
+
cpuScaling: (
|
|
1197
|
+
slug: string,
|
|
1198
|
+
environment: EnvironmentType,
|
|
1199
|
+
asgName: string | { Ref: string },
|
|
1200
|
+
targetCpu = 70,
|
|
1201
|
+
): { scalingPolicy: AutoScalingScalingPolicy; logicalId: string } => {
|
|
1202
|
+
return Compute.createScalingPolicy({
|
|
1203
|
+
slug,
|
|
1204
|
+
environment,
|
|
1205
|
+
autoScalingGroupName: asgName,
|
|
1206
|
+
policyType: 'TargetTrackingScaling',
|
|
1207
|
+
predefinedMetricType: 'ASGAverageCPUUtilization',
|
|
1208
|
+
targetValue: targetCpu,
|
|
1209
|
+
})
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Request count scaling policy (ALB)
|
|
1214
|
+
*/
|
|
1215
|
+
requestCountScaling: (
|
|
1216
|
+
slug: string,
|
|
1217
|
+
environment: EnvironmentType,
|
|
1218
|
+
asgName: string | { Ref: string },
|
|
1219
|
+
targetRequestCount = 1000,
|
|
1220
|
+
): { scalingPolicy: AutoScalingScalingPolicy; logicalId: string } => {
|
|
1221
|
+
return Compute.createScalingPolicy({
|
|
1222
|
+
slug,
|
|
1223
|
+
environment,
|
|
1224
|
+
autoScalingGroupName: asgName,
|
|
1225
|
+
policyType: 'TargetTrackingScaling',
|
|
1226
|
+
predefinedMetricType: 'ALBRequestCountPerTarget',
|
|
1227
|
+
targetValue: targetRequestCount,
|
|
1228
|
+
})
|
|
1229
|
+
},
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Secrets Manager integration utilities
|
|
1234
|
+
*/
|
|
1235
|
+
static readonly Secrets = {
|
|
1236
|
+
/**
|
|
1237
|
+
* Convert environment variables to ECS secrets configuration
|
|
1238
|
+
* This takes environment variable names and their corresponding Secrets Manager ARNs
|
|
1239
|
+
*/
|
|
1240
|
+
fromSecretsManager: (secrets: Record<string, string>): Array<{ name: string, valueFrom: string }> => {
|
|
1241
|
+
return Object.entries(secrets).map(([name, secretArn]) => ({
|
|
1242
|
+
name,
|
|
1243
|
+
valueFrom: secretArn,
|
|
1244
|
+
}))
|
|
1245
|
+
},
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Reference a specific key from a JSON secret
|
|
1249
|
+
* Format: arn:aws:secretsmanager:region:account:secret:name:json-key::
|
|
1250
|
+
*/
|
|
1251
|
+
fromJsonSecret: (secretArn: string, jsonKey: string): string => {
|
|
1252
|
+
return `${secretArn}:${jsonKey}::`
|
|
1253
|
+
},
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Reference a specific version of a secret
|
|
1257
|
+
* Format: arn:aws:secretsmanager:region:account:secret:name::version-id:
|
|
1258
|
+
*/
|
|
1259
|
+
fromSecretVersion: (secretArn: string, versionId: string): string => {
|
|
1260
|
+
return `${secretArn}::${versionId}:`
|
|
1261
|
+
},
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Reference a specific version stage of a secret
|
|
1265
|
+
* Format: arn:aws:secretsmanager:region:account:secret:name:::version-stage
|
|
1266
|
+
*/
|
|
1267
|
+
fromSecretVersionStage: (secretArn: string, versionStage: string): string => {
|
|
1268
|
+
return `${secretArn}:::${versionStage}`
|
|
1269
|
+
},
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Create IAM policy for Secrets Manager access
|
|
1273
|
+
*/
|
|
1274
|
+
createAccessPolicy: (secretArns: string[]): {
|
|
1275
|
+
PolicyName: string
|
|
1276
|
+
PolicyDocument: {
|
|
1277
|
+
Version: '2012-10-17'
|
|
1278
|
+
Statement: Array<{
|
|
1279
|
+
Effect: 'Allow' | 'Deny'
|
|
1280
|
+
Action: string[]
|
|
1281
|
+
Resource: string[]
|
|
1282
|
+
}>
|
|
1283
|
+
}
|
|
1284
|
+
} => ({
|
|
1285
|
+
PolicyName: 'SecretsManagerAccess',
|
|
1286
|
+
PolicyDocument: {
|
|
1287
|
+
Version: '2012-10-17' as const,
|
|
1288
|
+
Statement: [{
|
|
1289
|
+
Effect: 'Allow' as const,
|
|
1290
|
+
Action: [
|
|
1291
|
+
'secretsmanager:GetSecretValue',
|
|
1292
|
+
'secretsmanager:DescribeSecret',
|
|
1293
|
+
],
|
|
1294
|
+
Resource: secretArns,
|
|
1295
|
+
}],
|
|
1296
|
+
},
|
|
1297
|
+
}),
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Create IAM policy for KMS decryption (when secrets are encrypted with KMS)
|
|
1301
|
+
*/
|
|
1302
|
+
createKmsPolicy: (kmsKeyArns: string[]): {
|
|
1303
|
+
PolicyName: string
|
|
1304
|
+
PolicyDocument: {
|
|
1305
|
+
Version: '2012-10-17'
|
|
1306
|
+
Statement: Array<{
|
|
1307
|
+
Effect: 'Allow' | 'Deny'
|
|
1308
|
+
Action: string[]
|
|
1309
|
+
Resource: string[]
|
|
1310
|
+
}>
|
|
1311
|
+
}
|
|
1312
|
+
} => ({
|
|
1313
|
+
PolicyName: 'KMSDecryptAccess',
|
|
1314
|
+
PolicyDocument: {
|
|
1315
|
+
Version: '2012-10-17' as const,
|
|
1316
|
+
Statement: [{
|
|
1317
|
+
Effect: 'Allow' as const,
|
|
1318
|
+
Action: ['kms:Decrypt'],
|
|
1319
|
+
Resource: kmsKeyArns,
|
|
1320
|
+
}],
|
|
1321
|
+
},
|
|
1322
|
+
}),
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Build secret ARN from components
|
|
1326
|
+
*/
|
|
1327
|
+
buildSecretArn: (params: {
|
|
1328
|
+
region: string
|
|
1329
|
+
accountId: string
|
|
1330
|
+
secretName: string
|
|
1331
|
+
}): string => {
|
|
1332
|
+
return `arn:aws:secretsmanager:${params.region}:${params.accountId}:secret:${params.secretName}`
|
|
1333
|
+
},
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Build secret ARN pattern for wildcard matching
|
|
1337
|
+
* Useful for IAM policies
|
|
1338
|
+
*/
|
|
1339
|
+
buildSecretArnPattern: (params: {
|
|
1340
|
+
region?: string
|
|
1341
|
+
accountId?: string
|
|
1342
|
+
secretNamePrefix: string
|
|
1343
|
+
}): string => {
|
|
1344
|
+
const region = params.region || '*'
|
|
1345
|
+
const accountId = params.accountId || '*'
|
|
1346
|
+
return `arn:aws:secretsmanager:${region}:${accountId}:secret:${params.secretNamePrefix}*`
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Common environment secrets mapping
|
|
1351
|
+
* Maps common application environment variable names to secrets
|
|
1352
|
+
*/
|
|
1353
|
+
commonAppSecrets: (secretPrefix: string): Record<string, string> => ({
|
|
1354
|
+
DATABASE_URL: `${secretPrefix}/database-url`,
|
|
1355
|
+
DATABASE_PASSWORD: `${secretPrefix}/database-password`,
|
|
1356
|
+
REDIS_URL: `${secretPrefix}/redis-url`,
|
|
1357
|
+
REDIS_PASSWORD: `${secretPrefix}/redis-password`,
|
|
1358
|
+
API_KEY: `${secretPrefix}/api-key`,
|
|
1359
|
+
JWT_SECRET: `${secretPrefix}/jwt-secret`,
|
|
1360
|
+
ENCRYPTION_KEY: `${secretPrefix}/encryption-key`,
|
|
1361
|
+
AWS_ACCESS_KEY_ID: `${secretPrefix}/aws-access-key-id`,
|
|
1362
|
+
AWS_SECRET_ACCESS_KEY: `${secretPrefix}/aws-secret-access-key`,
|
|
1363
|
+
MAIL_PASSWORD: `${secretPrefix}/mail-password`,
|
|
1364
|
+
STRIPE_SECRET_KEY: `${secretPrefix}/stripe-secret-key`,
|
|
1365
|
+
STRIPE_WEBHOOK_SECRET: `${secretPrefix}/stripe-webhook-secret`,
|
|
1366
|
+
}),
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Create ECS Fargate service with full Secrets Manager integration
|
|
1371
|
+
*/
|
|
1372
|
+
static createFargateServiceWithSecrets(options: FargateServiceOptions & {
|
|
1373
|
+
secretArns?: string[]
|
|
1374
|
+
kmsKeyArns?: string[]
|
|
1375
|
+
}): {
|
|
1376
|
+
cluster: ECSCluster
|
|
1377
|
+
taskDefinition: ECSTaskDefinition
|
|
1378
|
+
service: ECSService
|
|
1379
|
+
taskRole: IAMRole
|
|
1380
|
+
executionRole: IAMRole
|
|
1381
|
+
clusterLogicalId: string
|
|
1382
|
+
taskDefinitionLogicalId: string
|
|
1383
|
+
serviceLogicalId: string
|
|
1384
|
+
taskRoleLogicalId: string
|
|
1385
|
+
executionRoleLogicalId: string
|
|
1386
|
+
} {
|
|
1387
|
+
const {
|
|
1388
|
+
secretArns = [],
|
|
1389
|
+
kmsKeyArns = [],
|
|
1390
|
+
...baseOptions
|
|
1391
|
+
} = options
|
|
1392
|
+
|
|
1393
|
+
// Create base Fargate service
|
|
1394
|
+
const result = Compute.createFargateService(baseOptions)
|
|
1395
|
+
|
|
1396
|
+
// Add Secrets Manager access policy to execution role if secrets are provided
|
|
1397
|
+
if (secretArns.length > 0) {
|
|
1398
|
+
if (!result.executionRole.Properties.Policies) {
|
|
1399
|
+
result.executionRole.Properties.Policies = []
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
result.executionRole.Properties.Policies.push(
|
|
1403
|
+
Compute.Secrets.createAccessPolicy(secretArns),
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
// Add KMS policy if KMS keys are specified
|
|
1407
|
+
if (kmsKeyArns.length > 0) {
|
|
1408
|
+
result.executionRole.Properties.Policies.push(
|
|
1409
|
+
Compute.Secrets.createKmsPolicy(kmsKeyArns),
|
|
1410
|
+
)
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return result
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Generate secret references for container environment
|
|
1419
|
+
* This is a helper to convert secret names to full ARN references
|
|
1420
|
+
*/
|
|
1421
|
+
static generateSecretReferences(params: {
|
|
1422
|
+
region: string
|
|
1423
|
+
accountId: string
|
|
1424
|
+
secretPrefix: string
|
|
1425
|
+
secrets: string[]
|
|
1426
|
+
}): Array<{ name: string, valueFrom: string }> {
|
|
1427
|
+
return params.secrets.map((secretName) => {
|
|
1428
|
+
const secretArn = `arn:aws:secretsmanager:${params.region}:${params.accountId}:secret:${params.secretPrefix}/${secretName}`
|
|
1429
|
+
return {
|
|
1430
|
+
name: secretName.toUpperCase().replace(/-/g, '_'),
|
|
1431
|
+
valueFrom: secretArn,
|
|
1432
|
+
}
|
|
1433
|
+
})
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Create environment secrets configuration for common patterns
|
|
1438
|
+
*/
|
|
1439
|
+
static readonly EnvSecrets = {
|
|
1440
|
+
/**
|
|
1441
|
+
* Database credentials as secrets
|
|
1442
|
+
*/
|
|
1443
|
+
database: (secretArn: string): Array<{ name: string, valueFrom: string }> => ([
|
|
1444
|
+
{ name: 'DB_HOST', valueFrom: `${secretArn}:host::` },
|
|
1445
|
+
{ name: 'DB_PORT', valueFrom: `${secretArn}:port::` },
|
|
1446
|
+
{ name: 'DB_USERNAME', valueFrom: `${secretArn}:username::` },
|
|
1447
|
+
{ name: 'DB_PASSWORD', valueFrom: `${secretArn}:password::` },
|
|
1448
|
+
{ name: 'DB_NAME', valueFrom: `${secretArn}:dbname::` },
|
|
1449
|
+
]),
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Redis credentials as secrets
|
|
1453
|
+
*/
|
|
1454
|
+
redis: (secretArn: string): Array<{ name: string, valueFrom: string }> => ([
|
|
1455
|
+
{ name: 'REDIS_HOST', valueFrom: `${secretArn}:host::` },
|
|
1456
|
+
{ name: 'REDIS_PORT', valueFrom: `${secretArn}:port::` },
|
|
1457
|
+
{ name: 'REDIS_PASSWORD', valueFrom: `${secretArn}:password::` },
|
|
1458
|
+
]),
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* API credentials as secrets
|
|
1462
|
+
*/
|
|
1463
|
+
apiCredentials: (secretArn: string): Array<{ name: string, valueFrom: string }> => ([
|
|
1464
|
+
{ name: 'API_KEY', valueFrom: `${secretArn}:apiKey::` },
|
|
1465
|
+
{ name: 'API_SECRET', valueFrom: `${secretArn}:apiSecret::` },
|
|
1466
|
+
]),
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Mail credentials as secrets
|
|
1470
|
+
*/
|
|
1471
|
+
mail: (secretArn: string): Array<{ name: string, valueFrom: string }> => ([
|
|
1472
|
+
{ name: 'MAIL_HOST', valueFrom: `${secretArn}:host::` },
|
|
1473
|
+
{ name: 'MAIL_PORT', valueFrom: `${secretArn}:port::` },
|
|
1474
|
+
{ name: 'MAIL_USERNAME', valueFrom: `${secretArn}:username::` },
|
|
1475
|
+
{ name: 'MAIL_PASSWORD', valueFrom: `${secretArn}:password::` },
|
|
1476
|
+
]),
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* AWS credentials as secrets (for cross-account access)
|
|
1480
|
+
*/
|
|
1481
|
+
awsCredentials: (secretArn: string): Array<{ name: string, valueFrom: string }> => ([
|
|
1482
|
+
{ name: 'AWS_ACCESS_KEY_ID', valueFrom: `${secretArn}:accessKeyId::` },
|
|
1483
|
+
{ name: 'AWS_SECRET_ACCESS_KEY', valueFrom: `${secretArn}:secretAccessKey::` },
|
|
1484
|
+
]),
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Create a JumpBox (Bastion Host) for SSH access to private resources
|
|
1489
|
+
*/
|
|
1490
|
+
static createJumpBox(options: {
|
|
1491
|
+
slug: string
|
|
1492
|
+
environment: EnvironmentType
|
|
1493
|
+
vpcId: string
|
|
1494
|
+
subnetId: string
|
|
1495
|
+
keyName: string
|
|
1496
|
+
instanceType?: string
|
|
1497
|
+
imageId?: string
|
|
1498
|
+
allowedCidrs?: string[]
|
|
1499
|
+
mountEfs?: {
|
|
1500
|
+
fileSystemId: string
|
|
1501
|
+
mountPath?: string
|
|
1502
|
+
}
|
|
1503
|
+
}): {
|
|
1504
|
+
instance: EC2Instance
|
|
1505
|
+
securityGroup: EC2SecurityGroup
|
|
1506
|
+
instanceProfile: any
|
|
1507
|
+
instanceRole: IAMRole
|
|
1508
|
+
instanceLogicalId: string
|
|
1509
|
+
securityGroupLogicalId: string
|
|
1510
|
+
instanceProfileLogicalId: string
|
|
1511
|
+
instanceRoleLogicalId: string
|
|
1512
|
+
resources: Record<string, any>
|
|
1513
|
+
} {
|
|
1514
|
+
const {
|
|
1515
|
+
slug,
|
|
1516
|
+
environment,
|
|
1517
|
+
vpcId,
|
|
1518
|
+
subnetId,
|
|
1519
|
+
keyName,
|
|
1520
|
+
instanceType = 't3.micro',
|
|
1521
|
+
imageId = 'ami-0c55b159cbfafe1f0', // Amazon Linux 2023
|
|
1522
|
+
allowedCidrs = ['0.0.0.0/0'],
|
|
1523
|
+
mountEfs,
|
|
1524
|
+
} = options
|
|
1525
|
+
|
|
1526
|
+
const resourceName = generateResourceName({
|
|
1527
|
+
slug,
|
|
1528
|
+
environment,
|
|
1529
|
+
resourceType: 'jumpbox',
|
|
1530
|
+
})
|
|
1531
|
+
|
|
1532
|
+
// Create security group for SSH access
|
|
1533
|
+
const securityGroupLogicalId = generateLogicalId(`${resourceName}-sg`)
|
|
1534
|
+
const securityGroup: EC2SecurityGroup = {
|
|
1535
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1536
|
+
Properties: {
|
|
1537
|
+
GroupName: `${resourceName}-sg`,
|
|
1538
|
+
GroupDescription: `Security group for ${resourceName} JumpBox SSH access`,
|
|
1539
|
+
VpcId: vpcId,
|
|
1540
|
+
SecurityGroupIngress: allowedCidrs.map(cidr => ({
|
|
1541
|
+
IpProtocol: 'tcp',
|
|
1542
|
+
FromPort: 22,
|
|
1543
|
+
ToPort: 22,
|
|
1544
|
+
CidrIp: cidr,
|
|
1545
|
+
Description: `SSH access from ${cidr}`,
|
|
1546
|
+
})),
|
|
1547
|
+
SecurityGroupEgress: [{
|
|
1548
|
+
IpProtocol: '-1',
|
|
1549
|
+
CidrIp: '0.0.0.0/0',
|
|
1550
|
+
Description: 'Allow all outbound traffic',
|
|
1551
|
+
}],
|
|
1552
|
+
Tags: [
|
|
1553
|
+
{ Key: 'Name', Value: `${resourceName}-sg` },
|
|
1554
|
+
{ Key: 'Environment', Value: environment },
|
|
1555
|
+
],
|
|
1556
|
+
},
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Create IAM role for instance
|
|
1560
|
+
const instanceRoleLogicalId = generateLogicalId(`${resourceName}-role`)
|
|
1561
|
+
const instanceRole: IAMRole = {
|
|
1562
|
+
Type: 'AWS::IAM::Role',
|
|
1563
|
+
Properties: {
|
|
1564
|
+
RoleName: `${resourceName}-role`,
|
|
1565
|
+
AssumeRolePolicyDocument: {
|
|
1566
|
+
Version: '2012-10-17',
|
|
1567
|
+
Statement: [{
|
|
1568
|
+
Effect: 'Allow',
|
|
1569
|
+
Principal: {
|
|
1570
|
+
Service: 'ec2.amazonaws.com',
|
|
1571
|
+
},
|
|
1572
|
+
Action: 'sts:AssumeRole',
|
|
1573
|
+
}],
|
|
1574
|
+
},
|
|
1575
|
+
ManagedPolicyArns: [
|
|
1576
|
+
'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore', // For SSM Session Manager
|
|
1577
|
+
],
|
|
1578
|
+
Policies: mountEfs
|
|
1579
|
+
? [{
|
|
1580
|
+
PolicyName: 'EFSAccess',
|
|
1581
|
+
PolicyDocument: {
|
|
1582
|
+
Version: '2012-10-17',
|
|
1583
|
+
Statement: [{
|
|
1584
|
+
Effect: 'Allow',
|
|
1585
|
+
Action: [
|
|
1586
|
+
'elasticfilesystem:ClientMount',
|
|
1587
|
+
'elasticfilesystem:ClientWrite',
|
|
1588
|
+
'elasticfilesystem:ClientRootAccess',
|
|
1589
|
+
],
|
|
1590
|
+
Resource: '*',
|
|
1591
|
+
}],
|
|
1592
|
+
},
|
|
1593
|
+
}]
|
|
1594
|
+
: undefined,
|
|
1595
|
+
},
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Create instance profile
|
|
1599
|
+
const instanceProfileLogicalId = generateLogicalId(`${resourceName}-profile`)
|
|
1600
|
+
const instanceProfile = {
|
|
1601
|
+
Type: 'AWS::IAM::InstanceProfile',
|
|
1602
|
+
Properties: {
|
|
1603
|
+
InstanceProfileName: `${resourceName}-profile`,
|
|
1604
|
+
Roles: [Fn.Ref(instanceRoleLogicalId)],
|
|
1605
|
+
},
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Build user data script
|
|
1609
|
+
let userDataScript = `#!/bin/bash
|
|
1610
|
+
yum update -y
|
|
1611
|
+
yum install -y amazon-efs-utils nfs-utils jq curl wget htop
|
|
1612
|
+
`
|
|
1613
|
+
|
|
1614
|
+
// Add EFS mount if specified
|
|
1615
|
+
if (mountEfs) {
|
|
1616
|
+
const mountPath = mountEfs.mountPath || '/mnt/efs'
|
|
1617
|
+
userDataScript += `
|
|
1618
|
+
# Mount EFS
|
|
1619
|
+
mkdir -p ${mountPath}
|
|
1620
|
+
mount -t efs ${mountEfs.fileSystemId}:/ ${mountPath}
|
|
1621
|
+
echo "${mountEfs.fileSystemId}:/ ${mountPath} efs defaults,_netdev 0 0" >> /etc/fstab
|
|
1622
|
+
`
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Create the JumpBox instance
|
|
1626
|
+
const instanceLogicalId = generateLogicalId(resourceName)
|
|
1627
|
+
const instance: EC2Instance = {
|
|
1628
|
+
Type: 'AWS::EC2::Instance',
|
|
1629
|
+
DependsOn: [instanceProfileLogicalId],
|
|
1630
|
+
Properties: {
|
|
1631
|
+
ImageId: imageId,
|
|
1632
|
+
InstanceType: instanceType,
|
|
1633
|
+
KeyName: keyName,
|
|
1634
|
+
SubnetId: subnetId,
|
|
1635
|
+
SecurityGroupIds: [Fn.Ref(securityGroupLogicalId)] as any,
|
|
1636
|
+
IamInstanceProfile: Fn.Ref(instanceProfileLogicalId) as any,
|
|
1637
|
+
UserData: Fn.Base64(userDataScript) as any,
|
|
1638
|
+
BlockDeviceMappings: [{
|
|
1639
|
+
DeviceName: '/dev/xvda',
|
|
1640
|
+
Ebs: {
|
|
1641
|
+
VolumeSize: 20,
|
|
1642
|
+
VolumeType: 'gp3',
|
|
1643
|
+
Encrypted: true,
|
|
1644
|
+
DeleteOnTermination: true,
|
|
1645
|
+
},
|
|
1646
|
+
}],
|
|
1647
|
+
Tags: [
|
|
1648
|
+
{ Key: 'Name', Value: resourceName },
|
|
1649
|
+
{ Key: 'Environment', Value: environment },
|
|
1650
|
+
{ Key: 'Purpose', Value: 'JumpBox/Bastion' },
|
|
1651
|
+
],
|
|
1652
|
+
},
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const resources: Record<string, any> = {
|
|
1656
|
+
[securityGroupLogicalId]: securityGroup,
|
|
1657
|
+
[instanceRoleLogicalId]: instanceRole,
|
|
1658
|
+
[instanceProfileLogicalId]: instanceProfile,
|
|
1659
|
+
[instanceLogicalId]: instance,
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return {
|
|
1663
|
+
instance,
|
|
1664
|
+
securityGroup,
|
|
1665
|
+
instanceProfile,
|
|
1666
|
+
instanceRole,
|
|
1667
|
+
instanceLogicalId,
|
|
1668
|
+
securityGroupLogicalId,
|
|
1669
|
+
instanceProfileLogicalId,
|
|
1670
|
+
instanceRoleLogicalId,
|
|
1671
|
+
resources,
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* JumpBox helper configurations
|
|
1677
|
+
*/
|
|
1678
|
+
static readonly JumpBox = {
|
|
1679
|
+
/**
|
|
1680
|
+
* Create JumpBox with EFS mount for file access
|
|
1681
|
+
*/
|
|
1682
|
+
withEfsMount: (params: {
|
|
1683
|
+
slug: string
|
|
1684
|
+
environment: EnvironmentType
|
|
1685
|
+
vpcId: string
|
|
1686
|
+
subnetId: string
|
|
1687
|
+
keyName: string
|
|
1688
|
+
fileSystemId: string
|
|
1689
|
+
mountPath?: string
|
|
1690
|
+
allowedCidrs?: string[]
|
|
1691
|
+
}): {
|
|
1692
|
+
instance: EC2Instance
|
|
1693
|
+
securityGroup: EC2SecurityGroup
|
|
1694
|
+
instanceProfile: any
|
|
1695
|
+
instanceRole: IAMRole
|
|
1696
|
+
instanceLogicalId: string
|
|
1697
|
+
securityGroupLogicalId: string
|
|
1698
|
+
instanceProfileLogicalId: string
|
|
1699
|
+
instanceRoleLogicalId: string
|
|
1700
|
+
resources: Record<string, any>
|
|
1701
|
+
} => {
|
|
1702
|
+
return Compute.createJumpBox({
|
|
1703
|
+
slug: params.slug,
|
|
1704
|
+
environment: params.environment,
|
|
1705
|
+
vpcId: params.vpcId,
|
|
1706
|
+
subnetId: params.subnetId,
|
|
1707
|
+
keyName: params.keyName,
|
|
1708
|
+
allowedCidrs: params.allowedCidrs,
|
|
1709
|
+
mountEfs: {
|
|
1710
|
+
fileSystemId: params.fileSystemId,
|
|
1711
|
+
mountPath: params.mountPath || '/mnt/efs',
|
|
1712
|
+
},
|
|
1713
|
+
})
|
|
1714
|
+
},
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Create minimal JumpBox (SSH only)
|
|
1718
|
+
*/
|
|
1719
|
+
minimal: (params: {
|
|
1720
|
+
slug: string
|
|
1721
|
+
environment: EnvironmentType
|
|
1722
|
+
vpcId: string
|
|
1723
|
+
subnetId: string
|
|
1724
|
+
keyName: string
|
|
1725
|
+
allowedCidrs?: string[]
|
|
1726
|
+
}): {
|
|
1727
|
+
instance: EC2Instance
|
|
1728
|
+
securityGroup: EC2SecurityGroup
|
|
1729
|
+
instanceProfile: any
|
|
1730
|
+
instanceRole: IAMRole
|
|
1731
|
+
instanceLogicalId: string
|
|
1732
|
+
securityGroupLogicalId: string
|
|
1733
|
+
instanceProfileLogicalId: string
|
|
1734
|
+
instanceRoleLogicalId: string
|
|
1735
|
+
resources: Record<string, any>
|
|
1736
|
+
} => {
|
|
1737
|
+
return Compute.createJumpBox({
|
|
1738
|
+
slug: params.slug,
|
|
1739
|
+
environment: params.environment,
|
|
1740
|
+
vpcId: params.vpcId,
|
|
1741
|
+
subnetId: params.subnetId,
|
|
1742
|
+
keyName: params.keyName,
|
|
1743
|
+
instanceType: 't3.nano',
|
|
1744
|
+
allowedCidrs: params.allowedCidrs,
|
|
1745
|
+
})
|
|
1746
|
+
},
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Create JumpBox with database tools
|
|
1750
|
+
*/
|
|
1751
|
+
withDatabaseTools: (params: {
|
|
1752
|
+
slug: string
|
|
1753
|
+
environment: EnvironmentType
|
|
1754
|
+
vpcId: string
|
|
1755
|
+
subnetId: string
|
|
1756
|
+
keyName: string
|
|
1757
|
+
allowedCidrs?: string[]
|
|
1758
|
+
}): {
|
|
1759
|
+
instance: EC2Instance
|
|
1760
|
+
securityGroup: EC2SecurityGroup
|
|
1761
|
+
instanceProfile: any
|
|
1762
|
+
instanceRole: IAMRole
|
|
1763
|
+
instanceLogicalId: string
|
|
1764
|
+
securityGroupLogicalId: string
|
|
1765
|
+
instanceProfileLogicalId: string
|
|
1766
|
+
instanceRoleLogicalId: string
|
|
1767
|
+
resources: Record<string, any>
|
|
1768
|
+
} => {
|
|
1769
|
+
const result = Compute.createJumpBox({
|
|
1770
|
+
slug: params.slug,
|
|
1771
|
+
environment: params.environment,
|
|
1772
|
+
vpcId: params.vpcId,
|
|
1773
|
+
subnetId: params.subnetId,
|
|
1774
|
+
keyName: params.keyName,
|
|
1775
|
+
allowedCidrs: params.allowedCidrs,
|
|
1776
|
+
})
|
|
1777
|
+
|
|
1778
|
+
// Modify user data to include database tools
|
|
1779
|
+
const userDataScript = `#!/bin/bash
|
|
1780
|
+
yum update -y
|
|
1781
|
+
yum install -y amazon-efs-utils nfs-utils jq curl wget htop
|
|
1782
|
+
|
|
1783
|
+
# Install PostgreSQL client
|
|
1784
|
+
amazon-linux-extras install postgresql14 -y
|
|
1785
|
+
|
|
1786
|
+
# Install MySQL client
|
|
1787
|
+
yum install -y mysql
|
|
1788
|
+
|
|
1789
|
+
# Install Redis CLI
|
|
1790
|
+
yum install -y redis
|
|
1791
|
+
|
|
1792
|
+
echo "Database tools installed!"
|
|
1793
|
+
`
|
|
1794
|
+
|
|
1795
|
+
result.instance.Properties.UserData = Fn.Base64(userDataScript) as any
|
|
1796
|
+
|
|
1797
|
+
return result
|
|
1798
|
+
},
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Allowed CIDRs for corporate VPNs (common patterns)
|
|
1802
|
+
*/
|
|
1803
|
+
commonCidrs: {
|
|
1804
|
+
any: ['0.0.0.0/0'] as const,
|
|
1805
|
+
privateOnly: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] as const,
|
|
1806
|
+
},
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Instance size mapping - human-readable sizes to AWS instance types
|
|
1811
|
+
* Provides Stacks configuration parity for "size" configuration option
|
|
1812
|
+
*/
|
|
1813
|
+
static readonly InstanceSize = {
|
|
1814
|
+
/**
|
|
1815
|
+
* Map human-readable size to EC2 instance type
|
|
1816
|
+
*/
|
|
1817
|
+
toInstanceType: (
|
|
1818
|
+
size: 'nano' | 'micro' | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge' | '4xlarge' | '8xlarge',
|
|
1819
|
+
family: 't3' | 't3a' | 'm6i' | 'c6i' | 'r6i' = 't3',
|
|
1820
|
+
): string => {
|
|
1821
|
+
return `${family}.${size}`
|
|
1822
|
+
},
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Size configurations with CPU and memory specs
|
|
1826
|
+
*/
|
|
1827
|
+
specs: {
|
|
1828
|
+
nano: { vcpu: 2, memory: 0.5, instanceType: 't3.nano' },
|
|
1829
|
+
micro: { vcpu: 2, memory: 1, instanceType: 't3.micro' },
|
|
1830
|
+
small: { vcpu: 2, memory: 2, instanceType: 't3.small' },
|
|
1831
|
+
medium: { vcpu: 2, memory: 4, instanceType: 't3.medium' },
|
|
1832
|
+
large: { vcpu: 2, memory: 8, instanceType: 't3.large' },
|
|
1833
|
+
xlarge: { vcpu: 4, memory: 16, instanceType: 't3.xlarge' },
|
|
1834
|
+
'2xlarge': { vcpu: 8, memory: 32, instanceType: 't3.2xlarge' },
|
|
1835
|
+
} as const,
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Get Fargate CPU/memory from size
|
|
1839
|
+
*/
|
|
1840
|
+
toFargateSpecs: (
|
|
1841
|
+
size: 'nano' | 'micro' | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge',
|
|
1842
|
+
): { cpu: string, memory: string } => {
|
|
1843
|
+
const mapping: Record<string, { cpu: string, memory: string }> = {
|
|
1844
|
+
nano: { cpu: '256', memory: '512' },
|
|
1845
|
+
micro: { cpu: '256', memory: '1024' },
|
|
1846
|
+
small: { cpu: '512', memory: '1024' },
|
|
1847
|
+
medium: { cpu: '1024', memory: '2048' },
|
|
1848
|
+
large: { cpu: '2048', memory: '4096' },
|
|
1849
|
+
xlarge: { cpu: '4096', memory: '8192' },
|
|
1850
|
+
'2xlarge': { cpu: '4096', memory: '16384' },
|
|
1851
|
+
}
|
|
1852
|
+
return mapping[size] || mapping.medium
|
|
1853
|
+
},
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Get Lambda memory from size
|
|
1857
|
+
*/
|
|
1858
|
+
toLambdaMemory: (
|
|
1859
|
+
size: 'nano' | 'micro' | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge',
|
|
1860
|
+
): number => {
|
|
1861
|
+
const mapping: Record<string, number> = {
|
|
1862
|
+
nano: 128,
|
|
1863
|
+
micro: 256,
|
|
1864
|
+
small: 512,
|
|
1865
|
+
medium: 1024,
|
|
1866
|
+
large: 2048,
|
|
1867
|
+
xlarge: 4096,
|
|
1868
|
+
'2xlarge': 8192,
|
|
1869
|
+
}
|
|
1870
|
+
return mapping[size] || 1024
|
|
1871
|
+
},
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* Presets for common workloads
|
|
1875
|
+
*/
|
|
1876
|
+
presets: {
|
|
1877
|
+
webServer: 't3.small',
|
|
1878
|
+
apiServer: 't3.medium',
|
|
1879
|
+
worker: 't3.medium',
|
|
1880
|
+
database: 'r6i.large',
|
|
1881
|
+
cache: 'r6i.medium',
|
|
1882
|
+
compute: 'c6i.large',
|
|
1883
|
+
general: 'm6i.medium',
|
|
1884
|
+
} as const,
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Disk configuration helpers
|
|
1889
|
+
* Provides Stacks configuration parity for disk options
|
|
1890
|
+
*/
|
|
1891
|
+
static readonly DiskConfig = {
|
|
1892
|
+
/**
|
|
1893
|
+
* Create EBS volume configuration
|
|
1894
|
+
*/
|
|
1895
|
+
create: (options: {
|
|
1896
|
+
size: number
|
|
1897
|
+
type?: 'standard' | 'ssd' | 'premium' | 'gp2' | 'gp3' | 'io1' | 'io2'
|
|
1898
|
+
encrypted?: boolean
|
|
1899
|
+
iops?: number
|
|
1900
|
+
throughput?: number
|
|
1901
|
+
deleteOnTermination?: boolean
|
|
1902
|
+
}): {
|
|
1903
|
+
VolumeSize: number
|
|
1904
|
+
VolumeType: string
|
|
1905
|
+
Encrypted: boolean
|
|
1906
|
+
Iops?: number
|
|
1907
|
+
Throughput?: number
|
|
1908
|
+
DeleteOnTermination: boolean
|
|
1909
|
+
} => {
|
|
1910
|
+
const { size, type = 'ssd', encrypted = true, iops, throughput, deleteOnTermination = true } = options
|
|
1911
|
+
|
|
1912
|
+
// Map human-readable types to AWS types
|
|
1913
|
+
const typeMapping: Record<string, string> = {
|
|
1914
|
+
standard: 'gp2',
|
|
1915
|
+
ssd: 'gp3',
|
|
1916
|
+
premium: 'io2',
|
|
1917
|
+
gp2: 'gp2',
|
|
1918
|
+
gp3: 'gp3',
|
|
1919
|
+
io1: 'io1',
|
|
1920
|
+
io2: 'io2',
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const volumeType = typeMapping[type] || 'gp3'
|
|
1924
|
+
|
|
1925
|
+
const config: any = {
|
|
1926
|
+
VolumeSize: size,
|
|
1927
|
+
VolumeType: volumeType,
|
|
1928
|
+
Encrypted: encrypted,
|
|
1929
|
+
DeleteOnTermination: deleteOnTermination,
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Add IOPS for provisioned IOPS types
|
|
1933
|
+
if ((volumeType === 'io1' || volumeType === 'io2' || volumeType === 'gp3') && iops) {
|
|
1934
|
+
config.Iops = iops
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Add throughput for gp3
|
|
1938
|
+
if (volumeType === 'gp3' && throughput) {
|
|
1939
|
+
config.Throughput = throughput
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return config
|
|
1943
|
+
},
|
|
1944
|
+
|
|
1945
|
+
/**
|
|
1946
|
+
* Common disk configurations
|
|
1947
|
+
*/
|
|
1948
|
+
presets: {
|
|
1949
|
+
/**
|
|
1950
|
+
* Standard SSD (20GB gp3)
|
|
1951
|
+
*/
|
|
1952
|
+
standard: {
|
|
1953
|
+
VolumeSize: 20,
|
|
1954
|
+
VolumeType: 'gp3',
|
|
1955
|
+
Encrypted: true,
|
|
1956
|
+
DeleteOnTermination: true,
|
|
1957
|
+
},
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Large storage (100GB gp3)
|
|
1961
|
+
*/
|
|
1962
|
+
large: {
|
|
1963
|
+
VolumeSize: 100,
|
|
1964
|
+
VolumeType: 'gp3',
|
|
1965
|
+
Encrypted: true,
|
|
1966
|
+
DeleteOnTermination: true,
|
|
1967
|
+
},
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* High performance (50GB io2)
|
|
1971
|
+
*/
|
|
1972
|
+
highPerformance: {
|
|
1973
|
+
VolumeSize: 50,
|
|
1974
|
+
VolumeType: 'io2',
|
|
1975
|
+
Iops: 3000,
|
|
1976
|
+
Encrypted: true,
|
|
1977
|
+
DeleteOnTermination: true,
|
|
1978
|
+
},
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Database optimized (100GB io2 with high IOPS)
|
|
1982
|
+
*/
|
|
1983
|
+
database: {
|
|
1984
|
+
VolumeSize: 100,
|
|
1985
|
+
VolumeType: 'io2',
|
|
1986
|
+
Iops: 10000,
|
|
1987
|
+
Encrypted: true,
|
|
1988
|
+
DeleteOnTermination: false,
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Spot instance configuration
|
|
1995
|
+
* Provides Stacks configuration parity for spot instances
|
|
1996
|
+
*/
|
|
1997
|
+
static readonly SpotConfig = {
|
|
1998
|
+
/**
|
|
1999
|
+
* Create spot instance specification for Launch Template
|
|
2000
|
+
*/
|
|
2001
|
+
create: (options: {
|
|
2002
|
+
maxPrice?: string
|
|
2003
|
+
spotInstanceType?: 'one-time' | 'persistent'
|
|
2004
|
+
interruptionBehavior?: 'hibernate' | 'stop' | 'terminate'
|
|
2005
|
+
blockDurationMinutes?: number
|
|
2006
|
+
}): {
|
|
2007
|
+
SpotOptions: {
|
|
2008
|
+
MaxPrice?: string
|
|
2009
|
+
SpotInstanceType?: string
|
|
2010
|
+
InstanceInterruptionBehavior?: string
|
|
2011
|
+
BlockDurationMinutes?: number
|
|
2012
|
+
}
|
|
2013
|
+
} => {
|
|
2014
|
+
const {
|
|
2015
|
+
maxPrice,
|
|
2016
|
+
spotInstanceType = 'one-time',
|
|
2017
|
+
interruptionBehavior = 'terminate',
|
|
2018
|
+
blockDurationMinutes,
|
|
2019
|
+
} = options
|
|
2020
|
+
|
|
2021
|
+
const spotOptions: any = {
|
|
2022
|
+
SpotInstanceType: spotInstanceType,
|
|
2023
|
+
InstanceInterruptionBehavior: interruptionBehavior,
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (maxPrice) {
|
|
2027
|
+
spotOptions.MaxPrice = maxPrice
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (blockDurationMinutes) {
|
|
2031
|
+
spotOptions.BlockDurationMinutes = blockDurationMinutes
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
return { SpotOptions: spotOptions }
|
|
2035
|
+
},
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Common spot instance configurations
|
|
2039
|
+
*/
|
|
2040
|
+
presets: {
|
|
2041
|
+
/**
|
|
2042
|
+
* Standard spot (80% on-demand price)
|
|
2043
|
+
*/
|
|
2044
|
+
standard: {
|
|
2045
|
+
spotInstanceType: 'one-time',
|
|
2046
|
+
interruptionBehavior: 'terminate',
|
|
2047
|
+
},
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* Persistent spot (for long-running workloads)
|
|
2051
|
+
*/
|
|
2052
|
+
persistent: {
|
|
2053
|
+
spotInstanceType: 'persistent',
|
|
2054
|
+
interruptionBehavior: 'stop',
|
|
2055
|
+
},
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* Cost-optimized (lower max price)
|
|
2059
|
+
*/
|
|
2060
|
+
costOptimized: {
|
|
2061
|
+
maxPrice: '0.05',
|
|
2062
|
+
spotInstanceType: 'one-time',
|
|
2063
|
+
interruptionBehavior: 'terminate',
|
|
2064
|
+
},
|
|
2065
|
+
},
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Mixed instances configuration for Auto Scaling Groups
|
|
2070
|
+
* Provides Stacks configuration parity for mixed instance fleets
|
|
2071
|
+
*/
|
|
2072
|
+
static readonly MixedInstances = {
|
|
2073
|
+
/**
|
|
2074
|
+
* Create mixed instances policy for ASG
|
|
2075
|
+
*/
|
|
2076
|
+
create: (options: {
|
|
2077
|
+
instanceTypes: Array<{ size: string, weight?: number }>
|
|
2078
|
+
baseCapacity?: number
|
|
2079
|
+
onDemandPercentage?: number
|
|
2080
|
+
spotAllocationStrategy?: 'lowest-price' | 'capacity-optimized' | 'capacity-optimized-prioritized'
|
|
2081
|
+
spotMaxPrice?: string
|
|
2082
|
+
}): {
|
|
2083
|
+
MixedInstancesPolicy: {
|
|
2084
|
+
InstancesDistribution: {
|
|
2085
|
+
OnDemandBaseCapacity: number
|
|
2086
|
+
OnDemandPercentageAboveBaseCapacity: number
|
|
2087
|
+
SpotAllocationStrategy: string
|
|
2088
|
+
SpotMaxPrice?: string
|
|
2089
|
+
}
|
|
2090
|
+
LaunchTemplate: {
|
|
2091
|
+
Overrides: Array<{
|
|
2092
|
+
InstanceType: string
|
|
2093
|
+
WeightedCapacity?: string
|
|
2094
|
+
}>
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
} => {
|
|
2098
|
+
const {
|
|
2099
|
+
instanceTypes,
|
|
2100
|
+
baseCapacity = 0,
|
|
2101
|
+
onDemandPercentage = 20,
|
|
2102
|
+
spotAllocationStrategy = 'capacity-optimized',
|
|
2103
|
+
spotMaxPrice,
|
|
2104
|
+
} = options
|
|
2105
|
+
|
|
2106
|
+
const distribution: any = {
|
|
2107
|
+
OnDemandBaseCapacity: baseCapacity,
|
|
2108
|
+
OnDemandPercentageAboveBaseCapacity: onDemandPercentage,
|
|
2109
|
+
SpotAllocationStrategy: spotAllocationStrategy,
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
if (spotMaxPrice) {
|
|
2113
|
+
distribution.SpotMaxPrice = spotMaxPrice
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const overrides = instanceTypes.map(({ size, weight }) => {
|
|
2117
|
+
const override: any = { InstanceType: Compute.InstanceSize.toInstanceType(size as any) }
|
|
2118
|
+
if (weight) {
|
|
2119
|
+
override.WeightedCapacity = String(weight)
|
|
2120
|
+
}
|
|
2121
|
+
return override
|
|
2122
|
+
})
|
|
2123
|
+
|
|
2124
|
+
return {
|
|
2125
|
+
MixedInstancesPolicy: {
|
|
2126
|
+
InstancesDistribution: distribution,
|
|
2127
|
+
LaunchTemplate: {
|
|
2128
|
+
Overrides: overrides,
|
|
2129
|
+
},
|
|
2130
|
+
},
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* Common mixed instance configurations
|
|
2136
|
+
*/
|
|
2137
|
+
presets: {
|
|
2138
|
+
/**
|
|
2139
|
+
* Cost-optimized (80% spot)
|
|
2140
|
+
*/
|
|
2141
|
+
costOptimized: {
|
|
2142
|
+
baseCapacity: 0,
|
|
2143
|
+
onDemandPercentage: 20,
|
|
2144
|
+
spotAllocationStrategy: 'lowest-price',
|
|
2145
|
+
instanceTypes: [
|
|
2146
|
+
{ size: 'small', weight: 1 },
|
|
2147
|
+
{ size: 'medium', weight: 2 },
|
|
2148
|
+
] as const,
|
|
2149
|
+
},
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Balanced (50% spot)
|
|
2153
|
+
*/
|
|
2154
|
+
balanced: {
|
|
2155
|
+
baseCapacity: 1,
|
|
2156
|
+
onDemandPercentage: 50,
|
|
2157
|
+
spotAllocationStrategy: 'capacity-optimized',
|
|
2158
|
+
instanceTypes: [
|
|
2159
|
+
{ size: 'medium', weight: 1 },
|
|
2160
|
+
{ size: 'large', weight: 2 },
|
|
2161
|
+
] as const,
|
|
2162
|
+
},
|
|
2163
|
+
|
|
2164
|
+
/**
|
|
2165
|
+
* High availability (20% spot)
|
|
2166
|
+
*/
|
|
2167
|
+
highAvailability: {
|
|
2168
|
+
baseCapacity: 2,
|
|
2169
|
+
onDemandPercentage: 80,
|
|
2170
|
+
spotAllocationStrategy: 'capacity-optimized-prioritized',
|
|
2171
|
+
instanceTypes: [
|
|
2172
|
+
{ size: 'medium', weight: 1 },
|
|
2173
|
+
] as const,
|
|
2174
|
+
},
|
|
2175
|
+
},
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Auto-scaling configuration helpers
|
|
2180
|
+
* Provides Stacks configuration parity for auto-scaling options
|
|
2181
|
+
*/
|
|
2182
|
+
static readonly AutoScalingConfig = {
|
|
2183
|
+
/**
|
|
2184
|
+
* Create auto-scaling configuration
|
|
2185
|
+
*/
|
|
2186
|
+
create: (options: {
|
|
2187
|
+
min: number
|
|
2188
|
+
max: number
|
|
2189
|
+
desired?: number
|
|
2190
|
+
scaleUpThreshold?: number
|
|
2191
|
+
scaleDownThreshold?: number
|
|
2192
|
+
cooldownSeconds?: number
|
|
2193
|
+
targetMetric?: 'cpu' | 'memory' | 'requests'
|
|
2194
|
+
}): {
|
|
2195
|
+
minSize: number
|
|
2196
|
+
maxSize: number
|
|
2197
|
+
desiredCapacity: number
|
|
2198
|
+
scalingPolicies: Array<{
|
|
2199
|
+
policyType: string
|
|
2200
|
+
targetValue: number
|
|
2201
|
+
predefinedMetricType: string
|
|
2202
|
+
scaleInCooldown: number
|
|
2203
|
+
scaleOutCooldown: number
|
|
2204
|
+
}>
|
|
2205
|
+
} => {
|
|
2206
|
+
const {
|
|
2207
|
+
min,
|
|
2208
|
+
max,
|
|
2209
|
+
desired = min,
|
|
2210
|
+
scaleUpThreshold = 70,
|
|
2211
|
+
scaleDownThreshold = 30,
|
|
2212
|
+
cooldownSeconds = 300,
|
|
2213
|
+
targetMetric = 'cpu',
|
|
2214
|
+
} = options
|
|
2215
|
+
|
|
2216
|
+
const metricMapping: Record<string, string> = {
|
|
2217
|
+
cpu: 'ASGAverageCPUUtilization',
|
|
2218
|
+
memory: 'ASGAverageMemoryUtilization',
|
|
2219
|
+
requests: 'ALBRequestCountPerTarget',
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
return {
|
|
2223
|
+
minSize: min,
|
|
2224
|
+
maxSize: max,
|
|
2225
|
+
desiredCapacity: desired,
|
|
2226
|
+
scalingPolicies: [
|
|
2227
|
+
{
|
|
2228
|
+
policyType: 'TargetTrackingScaling',
|
|
2229
|
+
targetValue: scaleUpThreshold,
|
|
2230
|
+
predefinedMetricType: metricMapping[targetMetric] || metricMapping.cpu,
|
|
2231
|
+
scaleInCooldown: cooldownSeconds,
|
|
2232
|
+
scaleOutCooldown: cooldownSeconds,
|
|
2233
|
+
},
|
|
2234
|
+
],
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* ECS auto-scaling configuration
|
|
2240
|
+
*/
|
|
2241
|
+
forEcs: (options: {
|
|
2242
|
+
min: number
|
|
2243
|
+
max: number
|
|
2244
|
+
cpuTarget?: number
|
|
2245
|
+
memoryTarget?: number
|
|
2246
|
+
}): {
|
|
2247
|
+
minCapacity: number
|
|
2248
|
+
maxCapacity: number
|
|
2249
|
+
targetTrackingPolicies: Array<{
|
|
2250
|
+
predefinedMetricType: string
|
|
2251
|
+
targetValue: number
|
|
2252
|
+
}>
|
|
2253
|
+
} => {
|
|
2254
|
+
const { min, max, cpuTarget = 70, memoryTarget } = options
|
|
2255
|
+
|
|
2256
|
+
const policies: Array<{ predefinedMetricType: string, targetValue: number }> = [
|
|
2257
|
+
{
|
|
2258
|
+
predefinedMetricType: 'ECSServiceAverageCPUUtilization',
|
|
2259
|
+
targetValue: cpuTarget,
|
|
2260
|
+
},
|
|
2261
|
+
]
|
|
2262
|
+
|
|
2263
|
+
if (memoryTarget) {
|
|
2264
|
+
policies.push({
|
|
2265
|
+
predefinedMetricType: 'ECSServiceAverageMemoryUtilization',
|
|
2266
|
+
targetValue: memoryTarget,
|
|
2267
|
+
})
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
return {
|
|
2271
|
+
minCapacity: min,
|
|
2272
|
+
maxCapacity: max,
|
|
2273
|
+
targetTrackingPolicies: policies,
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Common auto-scaling configurations
|
|
2279
|
+
*/
|
|
2280
|
+
presets: {
|
|
2281
|
+
/**
|
|
2282
|
+
* Small service (1-3 instances)
|
|
2283
|
+
*/
|
|
2284
|
+
small: {
|
|
2285
|
+
min: 1,
|
|
2286
|
+
max: 3,
|
|
2287
|
+
scaleUpThreshold: 70,
|
|
2288
|
+
scaleDownThreshold: 30,
|
|
2289
|
+
},
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Medium service (2-10 instances)
|
|
2293
|
+
*/
|
|
2294
|
+
medium: {
|
|
2295
|
+
min: 2,
|
|
2296
|
+
max: 10,
|
|
2297
|
+
scaleUpThreshold: 70,
|
|
2298
|
+
scaleDownThreshold: 30,
|
|
2299
|
+
},
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Large service (3-50 instances)
|
|
2303
|
+
*/
|
|
2304
|
+
large: {
|
|
2305
|
+
min: 3,
|
|
2306
|
+
max: 50,
|
|
2307
|
+
scaleUpThreshold: 60,
|
|
2308
|
+
scaleDownThreshold: 40,
|
|
2309
|
+
},
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* High availability (always 2+ instances)
|
|
2313
|
+
*/
|
|
2314
|
+
highAvailability: {
|
|
2315
|
+
min: 2,
|
|
2316
|
+
max: 20,
|
|
2317
|
+
scaleUpThreshold: 60,
|
|
2318
|
+
scaleDownThreshold: 30,
|
|
2319
|
+
},
|
|
2320
|
+
},
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* Load balancer configuration helpers
|
|
2325
|
+
* Provides Stacks configuration parity for load balancer options
|
|
2326
|
+
*/
|
|
2327
|
+
static readonly LoadBalancerConfig = {
|
|
2328
|
+
/**
|
|
2329
|
+
* Create load balancer health check configuration
|
|
2330
|
+
*/
|
|
2331
|
+
healthCheck: (options: {
|
|
2332
|
+
path?: string
|
|
2333
|
+
interval?: number
|
|
2334
|
+
timeout?: number
|
|
2335
|
+
healthyThreshold?: number
|
|
2336
|
+
unhealthyThreshold?: number
|
|
2337
|
+
protocol?: 'HTTP' | 'HTTPS' | 'TCP'
|
|
2338
|
+
}): {
|
|
2339
|
+
HealthCheckPath?: string
|
|
2340
|
+
HealthCheckIntervalSeconds: number
|
|
2341
|
+
HealthCheckTimeoutSeconds: number
|
|
2342
|
+
HealthyThresholdCount: number
|
|
2343
|
+
UnhealthyThresholdCount: number
|
|
2344
|
+
HealthCheckProtocol?: string
|
|
2345
|
+
} => {
|
|
2346
|
+
const {
|
|
2347
|
+
path = '/',
|
|
2348
|
+
interval = 30,
|
|
2349
|
+
timeout = 5,
|
|
2350
|
+
healthyThreshold = 2,
|
|
2351
|
+
unhealthyThreshold = 5,
|
|
2352
|
+
protocol = 'HTTP',
|
|
2353
|
+
} = options
|
|
2354
|
+
|
|
2355
|
+
const config: any = {
|
|
2356
|
+
HealthCheckIntervalSeconds: interval,
|
|
2357
|
+
HealthCheckTimeoutSeconds: timeout,
|
|
2358
|
+
HealthyThresholdCount: healthyThreshold,
|
|
2359
|
+
UnhealthyThresholdCount: unhealthyThreshold,
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
if (protocol !== 'TCP') {
|
|
2363
|
+
config.HealthCheckPath = path
|
|
2364
|
+
config.HealthCheckProtocol = protocol
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
return config
|
|
2368
|
+
},
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Common health check configurations
|
|
2372
|
+
*/
|
|
2373
|
+
presets: {
|
|
2374
|
+
/**
|
|
2375
|
+
* Standard HTTP health check
|
|
2376
|
+
*/
|
|
2377
|
+
standard: {
|
|
2378
|
+
path: '/health',
|
|
2379
|
+
interval: 30,
|
|
2380
|
+
timeout: 5,
|
|
2381
|
+
healthyThreshold: 2,
|
|
2382
|
+
unhealthyThreshold: 5,
|
|
2383
|
+
},
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Fast health check (for quick failover)
|
|
2387
|
+
*/
|
|
2388
|
+
fast: {
|
|
2389
|
+
path: '/health',
|
|
2390
|
+
interval: 10,
|
|
2391
|
+
timeout: 3,
|
|
2392
|
+
healthyThreshold: 2,
|
|
2393
|
+
unhealthyThreshold: 2,
|
|
2394
|
+
},
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* Relaxed health check (for slow-starting apps)
|
|
2398
|
+
*/
|
|
2399
|
+
relaxed: {
|
|
2400
|
+
path: '/health',
|
|
2401
|
+
interval: 60,
|
|
2402
|
+
timeout: 30,
|
|
2403
|
+
healthyThreshold: 2,
|
|
2404
|
+
unhealthyThreshold: 10,
|
|
2405
|
+
},
|
|
2406
|
+
},
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
/**
|
|
2410
|
+
* SSL configuration helpers
|
|
2411
|
+
* Provides Stacks configuration parity for SSL options
|
|
2412
|
+
*/
|
|
2413
|
+
static readonly SslConfig = {
|
|
2414
|
+
/**
|
|
2415
|
+
* Create SSL listener configuration
|
|
2416
|
+
*/
|
|
2417
|
+
httpsListener: (options: {
|
|
2418
|
+
certificateArn: string
|
|
2419
|
+
targetGroupArn: string
|
|
2420
|
+
port?: number
|
|
2421
|
+
sslPolicy?: string
|
|
2422
|
+
}): {
|
|
2423
|
+
Port: number
|
|
2424
|
+
Protocol: string
|
|
2425
|
+
Certificates: Array<{ CertificateArn: string }>
|
|
2426
|
+
SslPolicy: string
|
|
2427
|
+
DefaultActions: Array<{ Type: string, TargetGroupArn: string }>
|
|
2428
|
+
} => {
|
|
2429
|
+
const {
|
|
2430
|
+
certificateArn,
|
|
2431
|
+
targetGroupArn,
|
|
2432
|
+
port = 443,
|
|
2433
|
+
sslPolicy = 'ELBSecurityPolicy-TLS13-1-2-2021-06',
|
|
2434
|
+
} = options
|
|
2435
|
+
|
|
2436
|
+
return {
|
|
2437
|
+
Port: port,
|
|
2438
|
+
Protocol: 'HTTPS',
|
|
2439
|
+
Certificates: [{ CertificateArn: certificateArn }],
|
|
2440
|
+
SslPolicy: sslPolicy,
|
|
2441
|
+
DefaultActions: [{
|
|
2442
|
+
Type: 'forward',
|
|
2443
|
+
TargetGroupArn: targetGroupArn,
|
|
2444
|
+
}],
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* Create HTTP to HTTPS redirect listener
|
|
2450
|
+
*/
|
|
2451
|
+
httpRedirectListener: (port: number = 80): {
|
|
2452
|
+
Port: number
|
|
2453
|
+
Protocol: string
|
|
2454
|
+
DefaultActions: Array<{
|
|
2455
|
+
Type: string
|
|
2456
|
+
RedirectConfig: {
|
|
2457
|
+
Protocol: string
|
|
2458
|
+
Port: string
|
|
2459
|
+
StatusCode: string
|
|
2460
|
+
}
|
|
2461
|
+
}>
|
|
2462
|
+
} => ({
|
|
2463
|
+
Port: port,
|
|
2464
|
+
Protocol: 'HTTP',
|
|
2465
|
+
DefaultActions: [{
|
|
2466
|
+
Type: 'redirect',
|
|
2467
|
+
RedirectConfig: {
|
|
2468
|
+
Protocol: 'HTTPS',
|
|
2469
|
+
Port: '443',
|
|
2470
|
+
StatusCode: 'HTTP_301',
|
|
2471
|
+
},
|
|
2472
|
+
}],
|
|
2473
|
+
}),
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* SSL policies (TLS versions)
|
|
2477
|
+
*/
|
|
2478
|
+
policies: {
|
|
2479
|
+
tls13: 'ELBSecurityPolicy-TLS13-1-2-2021-06',
|
|
2480
|
+
tls12: 'ELBSecurityPolicy-TLS-1-2-Ext-2018-06',
|
|
2481
|
+
tls11: 'ELBSecurityPolicy-TLS-1-1-2017-01',
|
|
2482
|
+
fips: 'ELBSecurityPolicy-TLS-1-2-Ext-FIPS-2022-05',
|
|
2483
|
+
} as const,
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Functions configuration helpers (Lambda)
|
|
2488
|
+
* Provides Stacks configuration parity for functions configuration
|
|
2489
|
+
*/
|
|
2490
|
+
static readonly FunctionConfig = {
|
|
2491
|
+
/**
|
|
2492
|
+
* Create Lambda function configuration
|
|
2493
|
+
*/
|
|
2494
|
+
create: (options: {
|
|
2495
|
+
handler: string
|
|
2496
|
+
runtime?: string
|
|
2497
|
+
timeout?: number
|
|
2498
|
+
memorySize?: number
|
|
2499
|
+
environmentVariables?: Record<string, string>
|
|
2500
|
+
reservedConcurrency?: number
|
|
2501
|
+
}): {
|
|
2502
|
+
Handler: string
|
|
2503
|
+
Runtime: string
|
|
2504
|
+
Timeout: number
|
|
2505
|
+
MemorySize: number
|
|
2506
|
+
Environment?: { Variables: Record<string, string> }
|
|
2507
|
+
ReservedConcurrentExecutions?: number
|
|
2508
|
+
} => {
|
|
2509
|
+
const {
|
|
2510
|
+
handler,
|
|
2511
|
+
runtime = 'nodejs20.x',
|
|
2512
|
+
timeout = 30,
|
|
2513
|
+
memorySize = 256,
|
|
2514
|
+
environmentVariables,
|
|
2515
|
+
reservedConcurrency,
|
|
2516
|
+
} = options
|
|
2517
|
+
|
|
2518
|
+
const config: any = {
|
|
2519
|
+
Handler: handler,
|
|
2520
|
+
Runtime: runtime,
|
|
2521
|
+
Timeout: timeout,
|
|
2522
|
+
MemorySize: memorySize,
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
if (environmentVariables) {
|
|
2526
|
+
config.Environment = { Variables: environmentVariables }
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
if (reservedConcurrency) {
|
|
2530
|
+
config.ReservedConcurrentExecutions = reservedConcurrency
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
return config
|
|
2534
|
+
},
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* Runtime options
|
|
2538
|
+
*/
|
|
2539
|
+
runtimes: {
|
|
2540
|
+
nodejs20: 'nodejs20.x',
|
|
2541
|
+
nodejs18: 'nodejs18.x',
|
|
2542
|
+
python312: 'python3.12',
|
|
2543
|
+
python311: 'python3.11',
|
|
2544
|
+
java21: 'java21',
|
|
2545
|
+
java17: 'java17',
|
|
2546
|
+
go: 'provided.al2023',
|
|
2547
|
+
rust: 'provided.al2023',
|
|
2548
|
+
} as const,
|
|
2549
|
+
|
|
2550
|
+
/**
|
|
2551
|
+
* Common function configurations
|
|
2552
|
+
*/
|
|
2553
|
+
presets: {
|
|
2554
|
+
/**
|
|
2555
|
+
* API handler (fast response)
|
|
2556
|
+
*/
|
|
2557
|
+
api: {
|
|
2558
|
+
runtime: 'nodejs20.x',
|
|
2559
|
+
timeout: 30,
|
|
2560
|
+
memorySize: 256,
|
|
2561
|
+
},
|
|
2562
|
+
|
|
2563
|
+
/**
|
|
2564
|
+
* Worker (background processing)
|
|
2565
|
+
*/
|
|
2566
|
+
worker: {
|
|
2567
|
+
runtime: 'nodejs20.x',
|
|
2568
|
+
timeout: 300,
|
|
2569
|
+
memorySize: 512,
|
|
2570
|
+
},
|
|
2571
|
+
|
|
2572
|
+
/**
|
|
2573
|
+
* Cron job (scheduled task)
|
|
2574
|
+
*/
|
|
2575
|
+
cron: {
|
|
2576
|
+
runtime: 'nodejs20.x',
|
|
2577
|
+
timeout: 900,
|
|
2578
|
+
memorySize: 1024,
|
|
2579
|
+
},
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* Data processing (high memory)
|
|
2583
|
+
*/
|
|
2584
|
+
dataProcessing: {
|
|
2585
|
+
runtime: 'nodejs20.x',
|
|
2586
|
+
timeout: 900,
|
|
2587
|
+
memorySize: 3008,
|
|
2588
|
+
},
|
|
2589
|
+
},
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
/**
|
|
2593
|
+
* User data scripts for EC2 Server Mode (Forge-style)
|
|
2594
|
+
* Provides installation scripts for Bun, Node.js, Nginx, Caddy, PM2, etc.
|
|
2595
|
+
*/
|
|
2596
|
+
static readonly UserData = {
|
|
2597
|
+
/**
|
|
2598
|
+
* Generate complete user data script for app server
|
|
2599
|
+
*/
|
|
2600
|
+
generateAppServerScript: (options: {
|
|
2601
|
+
runtime?: 'bun' | 'node'
|
|
2602
|
+
runtimeVersion?: string
|
|
2603
|
+
webServer?: 'nginx' | 'caddy' | 'none'
|
|
2604
|
+
processManager?: 'pm2' | 'systemd'
|
|
2605
|
+
enableSsl?: boolean
|
|
2606
|
+
sslEmail?: string
|
|
2607
|
+
domain?: string
|
|
2608
|
+
appPort?: number
|
|
2609
|
+
installDatabaseClients?: boolean
|
|
2610
|
+
installRedis?: boolean
|
|
2611
|
+
extraPackages?: string[]
|
|
2612
|
+
}): string => {
|
|
2613
|
+
const {
|
|
2614
|
+
runtime = 'bun',
|
|
2615
|
+
runtimeVersion = 'latest',
|
|
2616
|
+
webServer = 'nginx',
|
|
2617
|
+
processManager = 'systemd',
|
|
2618
|
+
enableSsl = true,
|
|
2619
|
+
sslEmail = 'admin@example.com',
|
|
2620
|
+
domain,
|
|
2621
|
+
appPort = 3000,
|
|
2622
|
+
installDatabaseClients = false,
|
|
2623
|
+
installRedis = false,
|
|
2624
|
+
extraPackages = [],
|
|
2625
|
+
} = options
|
|
2626
|
+
|
|
2627
|
+
let script = `#!/bin/bash
|
|
2628
|
+
set -e
|
|
2629
|
+
|
|
2630
|
+
# Update system
|
|
2631
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
2632
|
+
apt-get update && apt-get upgrade -y
|
|
2633
|
+
|
|
2634
|
+
# Install basic tools
|
|
2635
|
+
apt-get install -y curl wget git jq htop unzip
|
|
2636
|
+
|
|
2637
|
+
`
|
|
2638
|
+
|
|
2639
|
+
// Install runtime
|
|
2640
|
+
if (runtime === 'bun') {
|
|
2641
|
+
script += Compute.UserData.Scripts.bun(runtimeVersion)
|
|
2642
|
+
}
|
|
2643
|
+
else {
|
|
2644
|
+
script += Compute.UserData.Scripts.nodeJs(runtimeVersion)
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Install web server
|
|
2648
|
+
if (webServer === 'nginx') {
|
|
2649
|
+
script += Compute.UserData.Scripts.nginx()
|
|
2650
|
+
if (domain && enableSsl) {
|
|
2651
|
+
script += Compute.UserData.Scripts.nginxProxy(domain, appPort)
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
else if (webServer === 'caddy') {
|
|
2655
|
+
script += Compute.UserData.Scripts.caddy()
|
|
2656
|
+
if (domain) {
|
|
2657
|
+
script += Compute.UserData.Scripts.caddyProxy(domain, appPort)
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// Install process manager
|
|
2662
|
+
if (processManager === 'pm2' && runtime === 'node') {
|
|
2663
|
+
script += Compute.UserData.Scripts.pm2()
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// Install Let's Encrypt if enabled
|
|
2667
|
+
if (enableSsl && webServer === 'nginx' && domain) {
|
|
2668
|
+
script += Compute.UserData.Scripts.letsEncrypt(domain, sslEmail)
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// Install database clients if requested
|
|
2672
|
+
if (installDatabaseClients) {
|
|
2673
|
+
script += Compute.UserData.Scripts.databaseClients()
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// Install Redis if requested
|
|
2677
|
+
if (installRedis) {
|
|
2678
|
+
script += Compute.UserData.Scripts.redis()
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// Install extra packages
|
|
2682
|
+
if (extraPackages.length > 0) {
|
|
2683
|
+
script += `\n# Install extra packages\napt-get install -y ${extraPackages.join(' ')}\n`
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
script += `\necho "Server setup complete!"\n`
|
|
2687
|
+
|
|
2688
|
+
return script
|
|
2689
|
+
},
|
|
2690
|
+
|
|
2691
|
+
/**
|
|
2692
|
+
* Individual installation scripts
|
|
2693
|
+
*/
|
|
2694
|
+
Scripts: {
|
|
2695
|
+
/**
|
|
2696
|
+
* Install Bun
|
|
2697
|
+
*/
|
|
2698
|
+
bun: (version: string = 'latest'): string => `
|
|
2699
|
+
# Install Bun
|
|
2700
|
+
curl -fsSL https://bun.sh/install | bash
|
|
2701
|
+
export BUN_INSTALL="$HOME/.bun"
|
|
2702
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2703
|
+
echo 'export BUN_INSTALL="$HOME/.bun"' >> /etc/profile.d/bun.sh
|
|
2704
|
+
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /etc/profile.d/bun.sh
|
|
2705
|
+
${version !== 'latest' ? `bun upgrade --version ${version}` : ''}
|
|
2706
|
+
bun --version
|
|
2707
|
+
`,
|
|
2708
|
+
|
|
2709
|
+
/**
|
|
2710
|
+
* Install Node.js via nvm
|
|
2711
|
+
*/
|
|
2712
|
+
nodeJs: (version: string = '20'): string => `
|
|
2713
|
+
# Install Node.js via nvm
|
|
2714
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
2715
|
+
export NVM_DIR="$HOME/.nvm"
|
|
2716
|
+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
|
2717
|
+
nvm install ${version}
|
|
2718
|
+
nvm use ${version}
|
|
2719
|
+
nvm alias default ${version}
|
|
2720
|
+
echo 'export NVM_DIR="$HOME/.nvm"' >> /etc/profile.d/nvm.sh
|
|
2721
|
+
echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> /etc/profile.d/nvm.sh
|
|
2722
|
+
node --version
|
|
2723
|
+
npm --version
|
|
2724
|
+
`,
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* Install Nginx
|
|
2728
|
+
*/
|
|
2729
|
+
nginx: (): string => `
|
|
2730
|
+
# Install Nginx
|
|
2731
|
+
apt-get install -y nginx
|
|
2732
|
+
systemctl enable nginx
|
|
2733
|
+
systemctl start nginx
|
|
2734
|
+
`,
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Configure Nginx as reverse proxy
|
|
2738
|
+
*/
|
|
2739
|
+
nginxProxy: (domain: string, port: number = 3000): string => `
|
|
2740
|
+
# Configure Nginx reverse proxy
|
|
2741
|
+
cat > /etc/nginx/sites-available/${domain} << 'NGINX_CONFIG'
|
|
2742
|
+
server {
|
|
2743
|
+
listen 80;
|
|
2744
|
+
server_name ${domain};
|
|
2745
|
+
|
|
2746
|
+
location / {
|
|
2747
|
+
proxy_pass http://127.0.0.1:${port};
|
|
2748
|
+
proxy_http_version 1.1;
|
|
2749
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
2750
|
+
proxy_set_header Connection 'upgrade';
|
|
2751
|
+
proxy_set_header Host $host;
|
|
2752
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
2753
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
2754
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
2755
|
+
proxy_cache_bypass $http_upgrade;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
NGINX_CONFIG
|
|
2759
|
+
|
|
2760
|
+
ln -sf /etc/nginx/sites-available/${domain} /etc/nginx/sites-enabled/
|
|
2761
|
+
rm -f /etc/nginx/sites-enabled/default
|
|
2762
|
+
nginx -t && systemctl reload nginx
|
|
2763
|
+
`,
|
|
2764
|
+
|
|
2765
|
+
/**
|
|
2766
|
+
* Install Caddy
|
|
2767
|
+
*/
|
|
2768
|
+
caddy: (): string => `
|
|
2769
|
+
# Install Caddy
|
|
2770
|
+
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
2771
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
2772
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
2773
|
+
apt-get update
|
|
2774
|
+
apt-get install -y caddy
|
|
2775
|
+
systemctl enable caddy
|
|
2776
|
+
`,
|
|
2777
|
+
|
|
2778
|
+
/**
|
|
2779
|
+
* Configure Caddy as reverse proxy
|
|
2780
|
+
*/
|
|
2781
|
+
caddyProxy: (domain: string, port: number = 3000): string => `
|
|
2782
|
+
# Configure Caddy reverse proxy
|
|
2783
|
+
cat > /etc/caddy/Caddyfile << 'CADDY_CONFIG'
|
|
2784
|
+
${domain} {
|
|
2785
|
+
reverse_proxy localhost:${port}
|
|
2786
|
+
}
|
|
2787
|
+
CADDY_CONFIG
|
|
2788
|
+
|
|
2789
|
+
systemctl restart caddy
|
|
2790
|
+
`,
|
|
2791
|
+
|
|
2792
|
+
/**
|
|
2793
|
+
* Install PM2
|
|
2794
|
+
*/
|
|
2795
|
+
pm2: (): string => `
|
|
2796
|
+
# Install PM2
|
|
2797
|
+
npm install -g pm2
|
|
2798
|
+
pm2 startup systemd -u root --hp /root
|
|
2799
|
+
`,
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* Install Let's Encrypt (certbot)
|
|
2803
|
+
*/
|
|
2804
|
+
letsEncrypt: (domain: string, email: string, staging: boolean = false): string => `
|
|
2805
|
+
# Install Certbot
|
|
2806
|
+
apt-get install -y certbot python3-certbot-nginx
|
|
2807
|
+
# Obtain SSL certificate
|
|
2808
|
+
certbot --nginx -d ${domain} --non-interactive --agree-tos -m ${email} ${staging ? '--staging' : ''}
|
|
2809
|
+
# Setup auto-renewal
|
|
2810
|
+
echo "0 0 * * * root certbot renew --quiet" > /etc/cron.d/certbot-renew
|
|
2811
|
+
`,
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* Install database clients
|
|
2815
|
+
*/
|
|
2816
|
+
databaseClients: (): string => `
|
|
2817
|
+
# Install database clients
|
|
2818
|
+
apt-get install -y postgresql-client mysql-client
|
|
2819
|
+
`,
|
|
2820
|
+
|
|
2821
|
+
/**
|
|
2822
|
+
* Install Redis (server and cli)
|
|
2823
|
+
*/
|
|
2824
|
+
redis: (): string => `
|
|
2825
|
+
# Install Redis
|
|
2826
|
+
apt-get install -y redis-server redis-tools
|
|
2827
|
+
systemctl enable redis-server
|
|
2828
|
+
systemctl start redis-server
|
|
2829
|
+
`,
|
|
2830
|
+
|
|
2831
|
+
/**
|
|
2832
|
+
* Create systemd service for app
|
|
2833
|
+
*/
|
|
2834
|
+
systemdService: (options: {
|
|
2835
|
+
serviceName: string
|
|
2836
|
+
description: string
|
|
2837
|
+
workingDirectory: string
|
|
2838
|
+
execStart: string
|
|
2839
|
+
user?: string
|
|
2840
|
+
environmentVars?: Record<string, string>
|
|
2841
|
+
}): string => {
|
|
2842
|
+
const {
|
|
2843
|
+
serviceName,
|
|
2844
|
+
description,
|
|
2845
|
+
workingDirectory,
|
|
2846
|
+
execStart,
|
|
2847
|
+
user = 'root',
|
|
2848
|
+
environmentVars = {},
|
|
2849
|
+
} = options
|
|
2850
|
+
|
|
2851
|
+
const envLines = Object.entries(environmentVars)
|
|
2852
|
+
.map(([key, value]) => `Environment="${key}=${value}"`)
|
|
2853
|
+
.join('\n')
|
|
2854
|
+
|
|
2855
|
+
return `
|
|
2856
|
+
# Create systemd service for ${serviceName}
|
|
2857
|
+
cat > /etc/systemd/system/${serviceName}.service << 'SERVICE_FILE'
|
|
2858
|
+
[Unit]
|
|
2859
|
+
Description=${description}
|
|
2860
|
+
After=network.target
|
|
2861
|
+
|
|
2862
|
+
[Service]
|
|
2863
|
+
Type=simple
|
|
2864
|
+
User=${user}
|
|
2865
|
+
WorkingDirectory=${workingDirectory}
|
|
2866
|
+
ExecStart=${execStart}
|
|
2867
|
+
Restart=on-failure
|
|
2868
|
+
RestartSec=10
|
|
2869
|
+
StandardOutput=syslog
|
|
2870
|
+
StandardError=syslog
|
|
2871
|
+
SyslogIdentifier=${serviceName}
|
|
2872
|
+
${envLines}
|
|
2873
|
+
|
|
2874
|
+
[Install]
|
|
2875
|
+
WantedBy=multi-user.target
|
|
2876
|
+
SERVICE_FILE
|
|
2877
|
+
|
|
2878
|
+
systemctl daemon-reload
|
|
2879
|
+
systemctl enable ${serviceName}
|
|
2880
|
+
systemctl start ${serviceName}
|
|
2881
|
+
`
|
|
2882
|
+
},
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* Setup swap file
|
|
2886
|
+
*/
|
|
2887
|
+
swapFile: (sizeGb: number = 2): string => `
|
|
2888
|
+
# Setup swap file
|
|
2889
|
+
fallocate -l ${sizeGb}G /swapfile
|
|
2890
|
+
chmod 600 /swapfile
|
|
2891
|
+
mkswap /swapfile
|
|
2892
|
+
swapon /swapfile
|
|
2893
|
+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
|
2894
|
+
`,
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* Setup firewall (ufw)
|
|
2898
|
+
*/
|
|
2899
|
+
firewall: (allowPorts: number[] = [22, 80, 443]): string => `
|
|
2900
|
+
# Setup firewall
|
|
2901
|
+
apt-get install -y ufw
|
|
2902
|
+
ufw default deny incoming
|
|
2903
|
+
ufw default allow outgoing
|
|
2904
|
+
${allowPorts.map(p => `ufw allow ${p}`).join('\n')}
|
|
2905
|
+
ufw --force enable
|
|
2906
|
+
`,
|
|
2907
|
+
},
|
|
2908
|
+
|
|
2909
|
+
/**
|
|
2910
|
+
* Preset user data configurations
|
|
2911
|
+
*/
|
|
2912
|
+
Presets: {
|
|
2913
|
+
/**
|
|
2914
|
+
* Bun app server with Nginx
|
|
2915
|
+
*/
|
|
2916
|
+
bunWithNginx: (domain: string, appPort: number = 3000): string =>
|
|
2917
|
+
Compute.UserData.generateAppServerScript({
|
|
2918
|
+
runtime: 'bun',
|
|
2919
|
+
webServer: 'nginx',
|
|
2920
|
+
processManager: 'systemd',
|
|
2921
|
+
domain,
|
|
2922
|
+
appPort,
|
|
2923
|
+
enableSsl: true,
|
|
2924
|
+
}),
|
|
2925
|
+
|
|
2926
|
+
/**
|
|
2927
|
+
* Bun app server with Caddy (auto SSL)
|
|
2928
|
+
*/
|
|
2929
|
+
bunWithCaddy: (domain: string, appPort: number = 3000): string =>
|
|
2930
|
+
Compute.UserData.generateAppServerScript({
|
|
2931
|
+
runtime: 'bun',
|
|
2932
|
+
webServer: 'caddy',
|
|
2933
|
+
processManager: 'systemd',
|
|
2934
|
+
domain,
|
|
2935
|
+
appPort,
|
|
2936
|
+
enableSsl: false, // Caddy handles SSL automatically
|
|
2937
|
+
}),
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* Node.js app server with PM2 and Nginx
|
|
2941
|
+
*/
|
|
2942
|
+
nodeWithPm2: (domain: string, appPort: number = 3000): string =>
|
|
2943
|
+
Compute.UserData.generateAppServerScript({
|
|
2944
|
+
runtime: 'node',
|
|
2945
|
+
webServer: 'nginx',
|
|
2946
|
+
processManager: 'pm2',
|
|
2947
|
+
domain,
|
|
2948
|
+
appPort,
|
|
2949
|
+
enableSsl: true,
|
|
2950
|
+
}),
|
|
2951
|
+
|
|
2952
|
+
/**
|
|
2953
|
+
* Minimal worker server (no web server)
|
|
2954
|
+
*/
|
|
2955
|
+
worker: (runtime: 'bun' | 'node' = 'bun'): string =>
|
|
2956
|
+
Compute.UserData.generateAppServerScript({
|
|
2957
|
+
runtime,
|
|
2958
|
+
webServer: 'none',
|
|
2959
|
+
processManager: 'systemd',
|
|
2960
|
+
enableSsl: false,
|
|
2961
|
+
}),
|
|
2962
|
+
},
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
/**
|
|
2966
|
+
* Create Elastic IP allocation
|
|
2967
|
+
*/
|
|
2968
|
+
static createElasticIp(options: {
|
|
2969
|
+
slug: string
|
|
2970
|
+
environment: EnvironmentType
|
|
2971
|
+
domain?: string
|
|
2972
|
+
instanceLogicalId?: string
|
|
2973
|
+
}): {
|
|
2974
|
+
eip: any
|
|
2975
|
+
eipAssociation?: any
|
|
2976
|
+
eipLogicalId: string
|
|
2977
|
+
associationLogicalId?: string
|
|
2978
|
+
resources: Record<string, any>
|
|
2979
|
+
} {
|
|
2980
|
+
const { slug, environment, domain, instanceLogicalId } = options
|
|
2981
|
+
|
|
2982
|
+
const resourceName = generateResourceName({
|
|
2983
|
+
slug,
|
|
2984
|
+
environment,
|
|
2985
|
+
resourceType: 'eip',
|
|
2986
|
+
})
|
|
2987
|
+
|
|
2988
|
+
const eipLogicalId = generateLogicalId(resourceName)
|
|
2989
|
+
|
|
2990
|
+
const eip = {
|
|
2991
|
+
Type: 'AWS::EC2::EIP',
|
|
2992
|
+
Properties: {
|
|
2993
|
+
Domain: 'vpc',
|
|
2994
|
+
Tags: [
|
|
2995
|
+
{ Key: 'Name', Value: resourceName },
|
|
2996
|
+
{ Key: 'Environment', Value: environment },
|
|
2997
|
+
...(domain ? [{ Key: 'Domain', Value: domain }] : []),
|
|
2998
|
+
],
|
|
2999
|
+
},
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
const resources: Record<string, any> = {
|
|
3003
|
+
[eipLogicalId]: eip,
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
let eipAssociation
|
|
3007
|
+
let associationLogicalId
|
|
3008
|
+
|
|
3009
|
+
if (instanceLogicalId) {
|
|
3010
|
+
associationLogicalId = generateLogicalId(`${resourceName}-assoc`)
|
|
3011
|
+
eipAssociation = {
|
|
3012
|
+
Type: 'AWS::EC2::EIPAssociation',
|
|
3013
|
+
Properties: {
|
|
3014
|
+
AllocationId: Fn.GetAtt(eipLogicalId, 'AllocationId'),
|
|
3015
|
+
InstanceId: Fn.Ref(instanceLogicalId),
|
|
3016
|
+
},
|
|
3017
|
+
}
|
|
3018
|
+
resources[associationLogicalId] = eipAssociation
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
return {
|
|
3022
|
+
eip,
|
|
3023
|
+
eipAssociation,
|
|
3024
|
+
eipLogicalId,
|
|
3025
|
+
associationLogicalId,
|
|
3026
|
+
resources,
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* Create complete Server Mode stack (Forge-style)
|
|
3032
|
+
* Creates EC2 instance with Elastic IP, security group, and IAM role
|
|
3033
|
+
*/
|
|
3034
|
+
static createServerModeStack(options: {
|
|
3035
|
+
slug: string
|
|
3036
|
+
environment: EnvironmentType
|
|
3037
|
+
vpcId: string
|
|
3038
|
+
subnetId: string
|
|
3039
|
+
instanceType?: string
|
|
3040
|
+
imageId?: string
|
|
3041
|
+
keyName: string
|
|
3042
|
+
domain?: string
|
|
3043
|
+
userData?: string
|
|
3044
|
+
allowedPorts?: number[]
|
|
3045
|
+
volumeSize?: number
|
|
3046
|
+
volumeType?: 'gp2' | 'gp3' | 'io1' | 'io2'
|
|
3047
|
+
}): {
|
|
3048
|
+
instance: EC2Instance
|
|
3049
|
+
securityGroup: EC2SecurityGroup
|
|
3050
|
+
eip: any
|
|
3051
|
+
eipAssociation: any
|
|
3052
|
+
instanceRole: IAMRole
|
|
3053
|
+
instanceProfile: any
|
|
3054
|
+
resources: Record<string, any>
|
|
3055
|
+
outputs: {
|
|
3056
|
+
instanceLogicalId: string
|
|
3057
|
+
securityGroupLogicalId: string
|
|
3058
|
+
eipLogicalId: string
|
|
3059
|
+
associationLogicalId: string
|
|
3060
|
+
roleLogicalId: string
|
|
3061
|
+
profileLogicalId: string
|
|
3062
|
+
}
|
|
3063
|
+
} {
|
|
3064
|
+
const {
|
|
3065
|
+
slug,
|
|
3066
|
+
environment,
|
|
3067
|
+
vpcId,
|
|
3068
|
+
subnetId,
|
|
3069
|
+
instanceType = 't3.small',
|
|
3070
|
+
imageId = 'ami-0c55b159cbfafe1f0', // Amazon Linux 2023
|
|
3071
|
+
keyName,
|
|
3072
|
+
domain,
|
|
3073
|
+
userData,
|
|
3074
|
+
allowedPorts = [22, 80, 443],
|
|
3075
|
+
volumeSize = 20,
|
|
3076
|
+
volumeType = 'gp3',
|
|
3077
|
+
} = options
|
|
3078
|
+
|
|
3079
|
+
const resources: Record<string, any> = {}
|
|
3080
|
+
|
|
3081
|
+
// Create security group
|
|
3082
|
+
const sgResourceName = generateResourceName({ slug, environment, resourceType: 'server-sg' })
|
|
3083
|
+
const securityGroupLogicalId = generateLogicalId(sgResourceName)
|
|
3084
|
+
|
|
3085
|
+
const securityGroup: EC2SecurityGroup = {
|
|
3086
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
3087
|
+
Properties: {
|
|
3088
|
+
GroupName: sgResourceName,
|
|
3089
|
+
GroupDescription: `Security group for ${slug} server`,
|
|
3090
|
+
VpcId: vpcId,
|
|
3091
|
+
SecurityGroupIngress: allowedPorts.map(port => ({
|
|
3092
|
+
IpProtocol: 'tcp',
|
|
3093
|
+
FromPort: port,
|
|
3094
|
+
ToPort: port,
|
|
3095
|
+
CidrIp: '0.0.0.0/0',
|
|
3096
|
+
Description: `Port ${port}`,
|
|
3097
|
+
})),
|
|
3098
|
+
SecurityGroupEgress: [{
|
|
3099
|
+
IpProtocol: '-1',
|
|
3100
|
+
CidrIp: '0.0.0.0/0',
|
|
3101
|
+
Description: 'Allow all outbound',
|
|
3102
|
+
}],
|
|
3103
|
+
Tags: [
|
|
3104
|
+
{ Key: 'Name', Value: sgResourceName },
|
|
3105
|
+
{ Key: 'Environment', Value: environment },
|
|
3106
|
+
],
|
|
3107
|
+
},
|
|
3108
|
+
}
|
|
3109
|
+
resources[securityGroupLogicalId] = securityGroup
|
|
3110
|
+
|
|
3111
|
+
// Create IAM role for instance
|
|
3112
|
+
const roleResourceName = generateResourceName({ slug, environment, resourceType: 'server-role' })
|
|
3113
|
+
const roleLogicalId = generateLogicalId(roleResourceName)
|
|
3114
|
+
|
|
3115
|
+
const instanceRole: IAMRole = {
|
|
3116
|
+
Type: 'AWS::IAM::Role',
|
|
3117
|
+
Properties: {
|
|
3118
|
+
RoleName: roleResourceName,
|
|
3119
|
+
AssumeRolePolicyDocument: {
|
|
3120
|
+
Version: '2012-10-17',
|
|
3121
|
+
Statement: [{
|
|
3122
|
+
Effect: 'Allow',
|
|
3123
|
+
Principal: { Service: 'ec2.amazonaws.com' },
|
|
3124
|
+
Action: 'sts:AssumeRole',
|
|
3125
|
+
}],
|
|
3126
|
+
},
|
|
3127
|
+
ManagedPolicyArns: [
|
|
3128
|
+
'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore',
|
|
3129
|
+
'arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy',
|
|
3130
|
+
],
|
|
3131
|
+
},
|
|
3132
|
+
}
|
|
3133
|
+
resources[roleLogicalId] = instanceRole
|
|
3134
|
+
|
|
3135
|
+
// Create instance profile
|
|
3136
|
+
const profileResourceName = generateResourceName({ slug, environment, resourceType: 'server-profile' })
|
|
3137
|
+
const profileLogicalId = generateLogicalId(profileResourceName)
|
|
3138
|
+
|
|
3139
|
+
const instanceProfile = {
|
|
3140
|
+
Type: 'AWS::IAM::InstanceProfile',
|
|
3141
|
+
Properties: {
|
|
3142
|
+
InstanceProfileName: profileResourceName,
|
|
3143
|
+
Roles: [Fn.Ref(roleLogicalId)],
|
|
3144
|
+
},
|
|
3145
|
+
}
|
|
3146
|
+
resources[profileLogicalId] = instanceProfile
|
|
3147
|
+
|
|
3148
|
+
// Create EC2 instance
|
|
3149
|
+
const instanceResourceName = generateResourceName({ slug, environment, resourceType: 'server' })
|
|
3150
|
+
const instanceLogicalId = generateLogicalId(instanceResourceName)
|
|
3151
|
+
|
|
3152
|
+
const instance: EC2Instance = {
|
|
3153
|
+
Type: 'AWS::EC2::Instance',
|
|
3154
|
+
DependsOn: [profileLogicalId],
|
|
3155
|
+
Properties: {
|
|
3156
|
+
ImageId: imageId,
|
|
3157
|
+
InstanceType: instanceType,
|
|
3158
|
+
KeyName: keyName,
|
|
3159
|
+
SubnetId: subnetId,
|
|
3160
|
+
SecurityGroupIds: [Fn.Ref(securityGroupLogicalId) as unknown as string],
|
|
3161
|
+
IamInstanceProfile: Fn.Ref(profileLogicalId) as unknown as string,
|
|
3162
|
+
BlockDeviceMappings: [{
|
|
3163
|
+
DeviceName: '/dev/xvda',
|
|
3164
|
+
Ebs: {
|
|
3165
|
+
VolumeSize: volumeSize,
|
|
3166
|
+
VolumeType: volumeType,
|
|
3167
|
+
Encrypted: true,
|
|
3168
|
+
DeleteOnTermination: true,
|
|
3169
|
+
},
|
|
3170
|
+
}],
|
|
3171
|
+
Tags: [
|
|
3172
|
+
{ Key: 'Name', Value: instanceResourceName },
|
|
3173
|
+
{ Key: 'Environment', Value: environment },
|
|
3174
|
+
...(domain ? [{ Key: 'Domain', Value: domain }] : []),
|
|
3175
|
+
],
|
|
3176
|
+
},
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
if (userData) {
|
|
3180
|
+
instance.Properties.UserData = Fn.Base64(userData) as any
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
resources[instanceLogicalId] = instance
|
|
3184
|
+
|
|
3185
|
+
// Create Elastic IP
|
|
3186
|
+
const { eip, eipAssociation, eipLogicalId, associationLogicalId, resources: eipResources } = Compute.createElasticIp({
|
|
3187
|
+
slug,
|
|
3188
|
+
environment,
|
|
3189
|
+
domain,
|
|
3190
|
+
instanceLogicalId,
|
|
3191
|
+
})
|
|
3192
|
+
|
|
3193
|
+
Object.assign(resources, eipResources)
|
|
3194
|
+
|
|
3195
|
+
return {
|
|
3196
|
+
instance,
|
|
3197
|
+
securityGroup,
|
|
3198
|
+
eip,
|
|
3199
|
+
eipAssociation,
|
|
3200
|
+
instanceRole,
|
|
3201
|
+
instanceProfile,
|
|
3202
|
+
resources,
|
|
3203
|
+
outputs: {
|
|
3204
|
+
instanceLogicalId,
|
|
3205
|
+
securityGroupLogicalId,
|
|
3206
|
+
eipLogicalId,
|
|
3207
|
+
associationLogicalId: associationLogicalId!,
|
|
3208
|
+
roleLogicalId,
|
|
3209
|
+
profileLogicalId,
|
|
3210
|
+
},
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* Server Mode presets for common server types
|
|
3216
|
+
*/
|
|
3217
|
+
static readonly ServerMode = {
|
|
3218
|
+
/**
|
|
3219
|
+
* Create web/app server
|
|
3220
|
+
*/
|
|
3221
|
+
webServer: (options: {
|
|
3222
|
+
slug: string
|
|
3223
|
+
environment: EnvironmentType
|
|
3224
|
+
vpcId: string
|
|
3225
|
+
subnetId: string
|
|
3226
|
+
keyName: string
|
|
3227
|
+
domain: string
|
|
3228
|
+
runtime?: 'bun' | 'node'
|
|
3229
|
+
webServer?: 'nginx' | 'caddy'
|
|
3230
|
+
}): {
|
|
3231
|
+
instance: EC2Instance
|
|
3232
|
+
securityGroup: EC2SecurityGroup
|
|
3233
|
+
eip: any
|
|
3234
|
+
eipAssociation: any
|
|
3235
|
+
instanceRole: IAMRole
|
|
3236
|
+
instanceProfile: any
|
|
3237
|
+
resources: Record<string, any>
|
|
3238
|
+
outputs: {
|
|
3239
|
+
instanceLogicalId: string
|
|
3240
|
+
securityGroupLogicalId: string
|
|
3241
|
+
eipLogicalId: string
|
|
3242
|
+
associationLogicalId: string
|
|
3243
|
+
roleLogicalId: string
|
|
3244
|
+
profileLogicalId: string
|
|
3245
|
+
}
|
|
3246
|
+
} => {
|
|
3247
|
+
const userData = Compute.UserData.generateAppServerScript({
|
|
3248
|
+
runtime: options.runtime || 'bun',
|
|
3249
|
+
webServer: options.webServer || 'nginx',
|
|
3250
|
+
domain: options.domain,
|
|
3251
|
+
enableSsl: true,
|
|
3252
|
+
})
|
|
3253
|
+
|
|
3254
|
+
return Compute.createServerModeStack({
|
|
3255
|
+
...options,
|
|
3256
|
+
userData,
|
|
3257
|
+
instanceType: 't3.small',
|
|
3258
|
+
allowedPorts: [22, 80, 443],
|
|
3259
|
+
})
|
|
3260
|
+
},
|
|
3261
|
+
|
|
3262
|
+
/**
|
|
3263
|
+
* Create worker server (no web server)
|
|
3264
|
+
*/
|
|
3265
|
+
workerServer: (options: {
|
|
3266
|
+
slug: string
|
|
3267
|
+
environment: EnvironmentType
|
|
3268
|
+
vpcId: string
|
|
3269
|
+
subnetId: string
|
|
3270
|
+
keyName: string
|
|
3271
|
+
runtime?: 'bun' | 'node'
|
|
3272
|
+
installRedis?: boolean
|
|
3273
|
+
}): {
|
|
3274
|
+
instance: EC2Instance
|
|
3275
|
+
securityGroup: EC2SecurityGroup
|
|
3276
|
+
eip: any
|
|
3277
|
+
eipAssociation: any
|
|
3278
|
+
instanceRole: IAMRole
|
|
3279
|
+
instanceProfile: any
|
|
3280
|
+
resources: Record<string, any>
|
|
3281
|
+
outputs: {
|
|
3282
|
+
instanceLogicalId: string
|
|
3283
|
+
securityGroupLogicalId: string
|
|
3284
|
+
eipLogicalId: string
|
|
3285
|
+
associationLogicalId: string
|
|
3286
|
+
roleLogicalId: string
|
|
3287
|
+
profileLogicalId: string
|
|
3288
|
+
}
|
|
3289
|
+
} => {
|
|
3290
|
+
const userData = Compute.UserData.generateAppServerScript({
|
|
3291
|
+
runtime: options.runtime || 'bun',
|
|
3292
|
+
webServer: 'none',
|
|
3293
|
+
installRedis: options.installRedis,
|
|
3294
|
+
})
|
|
3295
|
+
|
|
3296
|
+
return Compute.createServerModeStack({
|
|
3297
|
+
...options,
|
|
3298
|
+
userData,
|
|
3299
|
+
instanceType: 't3.medium',
|
|
3300
|
+
allowedPorts: [22],
|
|
3301
|
+
})
|
|
3302
|
+
},
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Create cache server (Redis)
|
|
3306
|
+
*/
|
|
3307
|
+
cacheServer: (options: {
|
|
3308
|
+
slug: string
|
|
3309
|
+
environment: EnvironmentType
|
|
3310
|
+
vpcId: string
|
|
3311
|
+
subnetId: string
|
|
3312
|
+
keyName: string
|
|
3313
|
+
}): {
|
|
3314
|
+
instance: EC2Instance
|
|
3315
|
+
securityGroup: EC2SecurityGroup
|
|
3316
|
+
eip: any
|
|
3317
|
+
eipAssociation: any
|
|
3318
|
+
instanceRole: IAMRole
|
|
3319
|
+
instanceProfile: any
|
|
3320
|
+
resources: Record<string, any>
|
|
3321
|
+
outputs: {
|
|
3322
|
+
instanceLogicalId: string
|
|
3323
|
+
securityGroupLogicalId: string
|
|
3324
|
+
eipLogicalId: string
|
|
3325
|
+
associationLogicalId: string
|
|
3326
|
+
roleLogicalId: string
|
|
3327
|
+
profileLogicalId: string
|
|
3328
|
+
}
|
|
3329
|
+
} => {
|
|
3330
|
+
const userData = `#!/bin/bash
|
|
3331
|
+
set -e
|
|
3332
|
+
apt-get update && apt-get upgrade -y
|
|
3333
|
+
apt-get install -y redis-server
|
|
3334
|
+
sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf
|
|
3335
|
+
systemctl enable redis-server
|
|
3336
|
+
systemctl restart redis-server
|
|
3337
|
+
echo "Redis server setup complete!"
|
|
3338
|
+
`
|
|
3339
|
+
|
|
3340
|
+
return Compute.createServerModeStack({
|
|
3341
|
+
...options,
|
|
3342
|
+
userData,
|
|
3343
|
+
instanceType: 't3.medium',
|
|
3344
|
+
allowedPorts: [22, 6379],
|
|
3345
|
+
})
|
|
3346
|
+
},
|
|
3347
|
+
}
|
|
3348
|
+
}
|