@stacksjs/ts-cloud-core 0.1.1

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.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +321 -0
  3. package/package.json +31 -0
  4. package/src/advanced-features.test.ts +465 -0
  5. package/src/aws/cloudformation.ts +421 -0
  6. package/src/aws/cloudfront.ts +158 -0
  7. package/src/aws/credentials.test.ts +132 -0
  8. package/src/aws/credentials.ts +545 -0
  9. package/src/aws/index.ts +87 -0
  10. package/src/aws/s3.test.ts +188 -0
  11. package/src/aws/s3.ts +1088 -0
  12. package/src/aws/signature.test.ts +670 -0
  13. package/src/aws/signature.ts +1155 -0
  14. package/src/backup/disaster-recovery.test.ts +726 -0
  15. package/src/backup/disaster-recovery.ts +500 -0
  16. package/src/backup/index.ts +34 -0
  17. package/src/backup/manager.test.ts +498 -0
  18. package/src/backup/manager.ts +432 -0
  19. package/src/cicd/circleci.ts +430 -0
  20. package/src/cicd/github-actions.ts +424 -0
  21. package/src/cicd/gitlab-ci.ts +255 -0
  22. package/src/cicd/index.ts +8 -0
  23. package/src/cli/history.ts +396 -0
  24. package/src/cli/index.ts +10 -0
  25. package/src/cli/progress.ts +458 -0
  26. package/src/cli/repl.ts +454 -0
  27. package/src/cli/suggestions.ts +327 -0
  28. package/src/cli/table.test.ts +319 -0
  29. package/src/cli/table.ts +332 -0
  30. package/src/cloudformation/builder.test.ts +327 -0
  31. package/src/cloudformation/builder.ts +378 -0
  32. package/src/cloudformation/builders/api-gateway.ts +449 -0
  33. package/src/cloudformation/builders/cache.ts +334 -0
  34. package/src/cloudformation/builders/cdn.ts +278 -0
  35. package/src/cloudformation/builders/compute.ts +485 -0
  36. package/src/cloudformation/builders/database.ts +392 -0
  37. package/src/cloudformation/builders/functions.ts +343 -0
  38. package/src/cloudformation/builders/messaging.ts +140 -0
  39. package/src/cloudformation/builders/monitoring.ts +300 -0
  40. package/src/cloudformation/builders/network.ts +264 -0
  41. package/src/cloudformation/builders/queue.ts +147 -0
  42. package/src/cloudformation/builders/security.ts +399 -0
  43. package/src/cloudformation/builders/storage.ts +285 -0
  44. package/src/cloudformation/index.ts +30 -0
  45. package/src/cloudformation/types.ts +173 -0
  46. package/src/compliance/aws-config.ts +543 -0
  47. package/src/compliance/cloudtrail.ts +376 -0
  48. package/src/compliance/compliance.test.ts +423 -0
  49. package/src/compliance/guardduty.ts +446 -0
  50. package/src/compliance/index.ts +66 -0
  51. package/src/compliance/security-hub.ts +456 -0
  52. package/src/containers/build-optimization.ts +416 -0
  53. package/src/containers/containers.test.ts +508 -0
  54. package/src/containers/image-scanning.ts +360 -0
  55. package/src/containers/index.ts +9 -0
  56. package/src/containers/registry.ts +293 -0
  57. package/src/containers/service-mesh.ts +520 -0
  58. package/src/database/database.test.ts +762 -0
  59. package/src/database/index.ts +9 -0
  60. package/src/database/migrations.ts +444 -0
  61. package/src/database/performance.ts +528 -0
  62. package/src/database/replicas.ts +534 -0
  63. package/src/database/users.ts +494 -0
  64. package/src/dependency-graph.ts +143 -0
  65. package/src/deployment/ab-testing.ts +582 -0
  66. package/src/deployment/blue-green.ts +452 -0
  67. package/src/deployment/canary.ts +500 -0
  68. package/src/deployment/deployment.test.ts +526 -0
  69. package/src/deployment/index.ts +61 -0
  70. package/src/deployment/progressive.ts +62 -0
  71. package/src/dns/dns.test.ts +641 -0
  72. package/src/dns/dnssec.ts +315 -0
  73. package/src/dns/index.ts +8 -0
  74. package/src/dns/resolver.ts +496 -0
  75. package/src/dns/routing.ts +593 -0
  76. package/src/email/advanced/analytics.ts +445 -0
  77. package/src/email/advanced/index.ts +11 -0
  78. package/src/email/advanced/rules.ts +465 -0
  79. package/src/email/advanced/scheduling.ts +352 -0
  80. package/src/email/advanced/search.ts +412 -0
  81. package/src/email/advanced/shared-mailboxes.ts +404 -0
  82. package/src/email/advanced/templates.ts +455 -0
  83. package/src/email/advanced/threading.ts +281 -0
  84. package/src/email/analytics.ts +467 -0
  85. package/src/email/bounce-handling.ts +425 -0
  86. package/src/email/email.test.ts +431 -0
  87. package/src/email/handlers/__tests__/inbound.test.ts +38 -0
  88. package/src/email/handlers/__tests__/outbound.test.ts +37 -0
  89. package/src/email/handlers/converter.ts +227 -0
  90. package/src/email/handlers/feedback.ts +228 -0
  91. package/src/email/handlers/inbound.ts +169 -0
  92. package/src/email/handlers/outbound.ts +178 -0
  93. package/src/email/index.ts +15 -0
  94. package/src/email/reputation.ts +303 -0
  95. package/src/email/templates.ts +352 -0
  96. package/src/errors/index.test.ts +434 -0
  97. package/src/errors/index.ts +416 -0
  98. package/src/health-checks/index.ts +40 -0
  99. package/src/index.ts +360 -0
  100. package/src/intrinsic-functions.ts +118 -0
  101. package/src/lambda/concurrency.ts +330 -0
  102. package/src/lambda/destinations.ts +345 -0
  103. package/src/lambda/dlq.ts +425 -0
  104. package/src/lambda/index.ts +11 -0
  105. package/src/lambda/lambda.test.ts +840 -0
  106. package/src/lambda/layers.ts +263 -0
  107. package/src/lambda/versions.ts +376 -0
  108. package/src/lambda/vpc.ts +399 -0
  109. package/src/local/config.ts +114 -0
  110. package/src/local/index.ts +6 -0
  111. package/src/local/mock-aws.ts +351 -0
  112. package/src/modules/ai.ts +340 -0
  113. package/src/modules/api.ts +478 -0
  114. package/src/modules/auth.ts +805 -0
  115. package/src/modules/cache.ts +417 -0
  116. package/src/modules/cdn.ts +1062 -0
  117. package/src/modules/communication.ts +1094 -0
  118. package/src/modules/compute.ts +3348 -0
  119. package/src/modules/database.ts +554 -0
  120. package/src/modules/deployment.ts +1079 -0
  121. package/src/modules/dns.ts +337 -0
  122. package/src/modules/email.ts +1538 -0
  123. package/src/modules/filesystem.ts +515 -0
  124. package/src/modules/index.ts +32 -0
  125. package/src/modules/messaging.ts +486 -0
  126. package/src/modules/monitoring.ts +2086 -0
  127. package/src/modules/network.ts +664 -0
  128. package/src/modules/parameter-store.ts +325 -0
  129. package/src/modules/permissions.ts +1081 -0
  130. package/src/modules/phone.ts +494 -0
  131. package/src/modules/queue.ts +1260 -0
  132. package/src/modules/redirects.ts +464 -0
  133. package/src/modules/registry.ts +699 -0
  134. package/src/modules/search.ts +401 -0
  135. package/src/modules/secrets.ts +416 -0
  136. package/src/modules/security.ts +731 -0
  137. package/src/modules/sms.ts +389 -0
  138. package/src/modules/storage.ts +1120 -0
  139. package/src/modules/workflow.ts +680 -0
  140. package/src/multi-account/config.ts +521 -0
  141. package/src/multi-account/index.ts +7 -0
  142. package/src/multi-account/manager.ts +427 -0
  143. package/src/multi-region/cross-region.ts +410 -0
  144. package/src/multi-region/index.ts +8 -0
  145. package/src/multi-region/manager.ts +483 -0
  146. package/src/multi-region/regions.ts +435 -0
  147. package/src/network-security/index.ts +48 -0
  148. package/src/observability/index.ts +9 -0
  149. package/src/observability/logs.ts +522 -0
  150. package/src/observability/metrics.ts +460 -0
  151. package/src/observability/observability.test.ts +782 -0
  152. package/src/observability/synthetics.ts +568 -0
  153. package/src/observability/xray.ts +358 -0
  154. package/src/phone/advanced/analytics.ts +349 -0
  155. package/src/phone/advanced/callbacks.ts +428 -0
  156. package/src/phone/advanced/index.ts +8 -0
  157. package/src/phone/advanced/ivr-builder.ts +504 -0
  158. package/src/phone/advanced/recording.ts +310 -0
  159. package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
  160. package/src/phone/handlers/incoming-call.ts +117 -0
  161. package/src/phone/handlers/missed-call.ts +116 -0
  162. package/src/phone/handlers/voicemail.ts +179 -0
  163. package/src/phone/index.ts +9 -0
  164. package/src/presets/api-backend.ts +134 -0
  165. package/src/presets/data-pipeline.ts +204 -0
  166. package/src/presets/extend.test.ts +295 -0
  167. package/src/presets/extend.ts +297 -0
  168. package/src/presets/fullstack-app.ts +144 -0
  169. package/src/presets/index.ts +27 -0
  170. package/src/presets/jamstack.ts +135 -0
  171. package/src/presets/microservices.ts +167 -0
  172. package/src/presets/ml-api.ts +208 -0
  173. package/src/presets/nodejs-server.ts +104 -0
  174. package/src/presets/nodejs-serverless.ts +114 -0
  175. package/src/presets/realtime-app.ts +184 -0
  176. package/src/presets/static-site.ts +64 -0
  177. package/src/presets/traditional-web-app.ts +339 -0
  178. package/src/presets/wordpress.ts +138 -0
  179. package/src/preview/github.test.ts +249 -0
  180. package/src/preview/github.ts +297 -0
  181. package/src/preview/index.ts +37 -0
  182. package/src/preview/manager.test.ts +440 -0
  183. package/src/preview/manager.ts +326 -0
  184. package/src/preview/notifications.test.ts +582 -0
  185. package/src/preview/notifications.ts +341 -0
  186. package/src/queue/batch-processing.ts +402 -0
  187. package/src/queue/dlq-monitoring.ts +402 -0
  188. package/src/queue/fifo.ts +342 -0
  189. package/src/queue/index.ts +9 -0
  190. package/src/queue/management.ts +428 -0
  191. package/src/queue/queue.test.ts +429 -0
  192. package/src/resource-mgmt/index.ts +39 -0
  193. package/src/resource-naming.ts +62 -0
  194. package/src/s3/index.ts +523 -0
  195. package/src/schema/cloud-config.schema.json +554 -0
  196. package/src/schema/index.ts +68 -0
  197. package/src/security/certificate-manager.ts +492 -0
  198. package/src/security/index.ts +9 -0
  199. package/src/security/scanning.ts +545 -0
  200. package/src/security/secrets-manager.ts +476 -0
  201. package/src/security/secrets-rotation.ts +456 -0
  202. package/src/security/security.test.ts +738 -0
  203. package/src/sms/advanced/ab-testing.ts +389 -0
  204. package/src/sms/advanced/analytics.ts +336 -0
  205. package/src/sms/advanced/campaigns.ts +523 -0
  206. package/src/sms/advanced/chatbot.ts +224 -0
  207. package/src/sms/advanced/index.ts +10 -0
  208. package/src/sms/advanced/link-tracking.ts +248 -0
  209. package/src/sms/advanced/mms.ts +308 -0
  210. package/src/sms/handlers/__tests__/send.test.ts +40 -0
  211. package/src/sms/handlers/delivery-status.ts +133 -0
  212. package/src/sms/handlers/receive.ts +162 -0
  213. package/src/sms/handlers/send.ts +174 -0
  214. package/src/sms/index.ts +9 -0
  215. package/src/stack-diff.ts +389 -0
  216. package/src/static-site/index.ts +85 -0
  217. package/src/template-builder.ts +110 -0
  218. package/src/template-validator.ts +574 -0
  219. package/src/utils/cache.ts +291 -0
  220. package/src/utils/diff.ts +269 -0
  221. package/src/utils/hash.ts +227 -0
  222. package/src/utils/index.ts +8 -0
  223. package/src/utils/parallel.ts +294 -0
  224. package/src/validators/credentials.test.ts +274 -0
  225. package/src/validators/credentials.ts +233 -0
  226. package/src/validators/quotas.test.ts +434 -0
  227. package/src/validators/quotas.ts +217 -0
  228. package/test/ai.test.ts +327 -0
  229. package/test/api.test.ts +511 -0
  230. package/test/auth.test.ts +632 -0
  231. package/test/cache.test.ts +406 -0
  232. package/test/cdn.test.ts +247 -0
  233. package/test/compute.test.ts +861 -0
  234. package/test/database.test.ts +523 -0
  235. package/test/deployment.test.ts +499 -0
  236. package/test/dns.test.ts +270 -0
  237. package/test/email.test.ts +439 -0
  238. package/test/filesystem.test.ts +382 -0
  239. package/test/integration.test.ts +350 -0
  240. package/test/messaging.test.ts +514 -0
  241. package/test/monitoring.test.ts +634 -0
  242. package/test/network.test.ts +425 -0
  243. package/test/permissions.test.ts +488 -0
  244. package/test/queue.test.ts +484 -0
  245. package/test/registry.test.ts +306 -0
  246. package/test/security.test.ts +462 -0
  247. package/test/storage.test.ts +463 -0
  248. package/test/template-validator.test.ts +559 -0
  249. package/test/workflow.test.ts +592 -0
  250. package/tsconfig.json +16 -0
  251. package/tsconfig.tsbuildinfo +1 -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
+ }