kybernus 2.2.0 → 2.3.0

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 (101) hide show
  1. package/package.json +1 -1
  2. package/templates/java-spring/clean/infra/main.tf.hbs +42 -18
  3. package/templates/java-spring/clean/infra/modules/ecs/main.tf.hbs +217 -6
  4. package/templates/java-spring/clean/infra/modules/rds/main.tf.hbs +15 -15
  5. package/templates/java-spring/clean/infra/modules/vpc/main.tf.hbs +170 -30
  6. package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +89 -0
  7. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/web/payment/PaymentController.java.hbs +78 -0
  8. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +7 -0
  9. package/templates/java-spring/hexagonal/infra/main.tf.hbs +42 -18
  10. package/templates/java-spring/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  11. package/templates/java-spring/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  12. package/templates/java-spring/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  13. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/inbound/web/PaymentController.java.hbs +78 -0
  14. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/stripe/StripeAdapter.java.hbs +76 -0
  15. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +90 -0
  16. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +7 -0
  17. package/templates/java-spring/mvc/infra/main.tf.hbs +42 -18
  18. package/templates/java-spring/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  19. package/templates/java-spring/mvc/infra/modules/rds/main.tf.hbs +15 -15
  20. package/templates/java-spring/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  21. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +42 -53
  22. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +105 -23
  23. package/templates/nestjs/clean/infra/main.tf.hbs +42 -18
  24. package/templates/nestjs/clean/infra/modules/ecs/main.tf.hbs +217 -6
  25. package/templates/nestjs/clean/infra/modules/rds/main.tf.hbs +15 -15
  26. package/templates/nestjs/clean/infra/modules/vpc/main.tf.hbs +170 -30
  27. package/templates/nestjs/clean/src/app.module.ts.hbs +3 -1
  28. package/templates/nestjs/clean/src/application/payment.service.ts.hbs +90 -0
  29. package/templates/nestjs/clean/src/infrastructure/http/payment.controller.ts.hbs +46 -0
  30. package/templates/nestjs/clean/src/infrastructure/stripe.provider.ts.hbs +51 -0
  31. package/templates/nestjs/clean/src/main.ts.hbs +13 -4
  32. package/templates/nestjs/clean/src/payment.module.ts.hbs +23 -0
  33. package/templates/nestjs/hexagonal/infra/main.tf.hbs +42 -18
  34. package/templates/nestjs/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  35. package/templates/nestjs/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  36. package/templates/nestjs/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  37. package/templates/nestjs/hexagonal/src/adapters/inbound/payment.controller.ts.hbs +46 -0
  38. package/templates/nestjs/hexagonal/src/adapters/outbound/stripe.adapter.ts.hbs +54 -0
  39. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -0
  40. package/templates/nestjs/hexagonal/src/core/payment.service.ts.hbs +90 -0
  41. package/templates/nestjs/hexagonal/src/main.ts.hbs +13 -4
  42. package/templates/nestjs/hexagonal/src/payment.module.ts.hbs +23 -0
  43. package/templates/nestjs/mvc/infra/main.tf.hbs +42 -18
  44. package/templates/nestjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  45. package/templates/nestjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
  46. package/templates/nestjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  47. package/templates/nestjs/mvc/src/main.ts.hbs +6 -3
  48. package/templates/nestjs/mvc/src/payments/payments.controller.ts.hbs +33 -8
  49. package/templates/nestjs/mvc/src/payments/payments.service.ts.hbs +66 -22
  50. package/templates/nextjs/mvc/infra/main.tf.hbs +42 -18
  51. package/templates/nextjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  52. package/templates/nextjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
  53. package/templates/nextjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  54. package/templates/nextjs/mvc/src/app/api/checkout/route.ts.hbs +42 -13
  55. package/templates/nextjs/mvc/src/app/api/portal/route.ts.hbs +36 -0
  56. package/templates/nextjs/mvc/src/app/api/webhook/route.ts.hbs +32 -20
  57. package/templates/nodejs-express/clean/infra/main.tf.hbs +42 -18
  58. package/templates/nodejs-express/clean/infra/modules/ecs/main.tf.hbs +217 -6
  59. package/templates/nodejs-express/clean/infra/modules/rds/main.tf.hbs +15 -15
  60. package/templates/nodejs-express/clean/infra/modules/vpc/main.tf.hbs +170 -30
  61. package/templates/nodejs-express/clean/src/application/services/PaymentService.ts.hbs +98 -0
  62. package/templates/nodejs-express/clean/src/index.ts.hbs +29 -8
  63. package/templates/nodejs-express/clean/src/infrastructure/http/controllers/PaymentController.ts.hbs +57 -0
  64. package/templates/nodejs-express/clean/src/infrastructure/providers/StripeProvider.ts.hbs +45 -0
  65. package/templates/nodejs-express/hexagonal/infra/main.tf.hbs +42 -18
  66. package/templates/nodejs-express/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  67. package/templates/nodejs-express/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  68. package/templates/nodejs-express/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  69. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +57 -0
  70. package/templates/nodejs-express/hexagonal/src/adapters/outbound/StripeAdapter.ts.hbs +48 -0
  71. package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +89 -0
  72. package/templates/nodejs-express/hexagonal/src/index.ts.hbs +28 -8
  73. package/templates/nodejs-express/mvc/infra/main.tf.hbs +42 -18
  74. package/templates/nodejs-express/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  75. package/templates/nodejs-express/mvc/infra/modules/rds/main.tf.hbs +15 -15
  76. package/templates/nodejs-express/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  77. package/templates/nodejs-express/mvc/src/app.ts.hbs +11 -2
  78. package/templates/nodejs-express/mvc/src/controllers/payments.controller.ts.hbs +31 -47
  79. package/templates/nodejs-express/mvc/src/services/stripe.service.ts.hbs +66 -49
  80. package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +85 -0
  81. package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +64 -0
  82. package/templates/python-fastapi/clean/app/infrastructure/stripe_provider.py.hbs +44 -0
  83. package/templates/python-fastapi/clean/app/main.py.hbs +8 -5
  84. package/templates/python-fastapi/clean/infra/main.tf.hbs +42 -18
  85. package/templates/python-fastapi/clean/infra/modules/ecs/main.tf.hbs +217 -6
  86. package/templates/python-fastapi/clean/infra/modules/rds/main.tf.hbs +15 -15
  87. package/templates/python-fastapi/clean/infra/modules/vpc/main.tf.hbs +170 -30
  88. package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +64 -0
  89. package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +44 -0
  90. package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +81 -0
  91. package/templates/python-fastapi/hexagonal/app/main.py.hbs +9 -3
  92. package/templates/python-fastapi/hexagonal/infra/main.tf.hbs +42 -18
  93. package/templates/python-fastapi/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  94. package/templates/python-fastapi/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  95. package/templates/python-fastapi/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  96. package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +70 -35
  97. package/templates/python-fastapi/mvc/app/services/stripe_service.py.hbs +58 -0
  98. package/templates/python-fastapi/mvc/infra/main.tf.hbs +42 -18
  99. package/templates/python-fastapi/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  100. package/templates/python-fastapi/mvc/infra/modules/rds/main.tf.hbs +15 -15
  101. package/templates/python-fastapi/mvc/infra/modules/vpc/main.tf.hbs +170 -30
@@ -1,20 +1,15 @@
1
1
  package {{packageName}}.controller;
2
2
 
3
- import com.stripe.exception.SignatureVerificationException;
4
- import com.stripe.model.Event;
5
- import com.stripe.net.Webhook;
6
- import {{packageName}}.model.User;
7
- import {{packageName}}.repository.UserRepository;
8
3
  import {{packageName}}.service.StripeService;
9
4
  import org.slf4j.Logger;
10
5
  import org.slf4j.LoggerFactory;
11
- import org.springframework.beans.factory.annotation.Value;
12
6
  import org.springframework.http.HttpStatus;
13
7
  import org.springframework.http.ResponseEntity;
8
+ import org.springframework.security.core.annotation.AuthenticationPrincipal;
9
+ import org.springframework.security.core.userdetails.UserDetails;
14
10
  import org.springframework.web.bind.annotation.*;
15
11
 
16
12
  import java.util.Map;
17
- import java.util.Optional;
18
13
 
19
14
  @RestController
20
15
  @RequestMapping("/api/payments")
@@ -22,71 +17,65 @@ public class PaymentsController {
22
17
 
23
18
  private static final Logger logger = LoggerFactory.getLogger(PaymentsController.class);
24
19
 
25
- @Value("${stripe.webhook-secret}")
26
- private String webhookSecret;
27
-
28
20
  private final StripeService stripeService;
29
- private final UserRepository userRepository;
30
21
 
31
- public PaymentsController(StripeService stripeService, UserRepository userRepository) {
22
+ public PaymentsController(StripeService stripeService) {
32
23
  this.stripeService = stripeService;
33
- this.userRepository = userRepository;
34
24
  }
35
25
 
26
+ /**
27
+ * POST /api/payments/checkout
28
+ * Requires authentication. Creates a Stripe Checkout session.
29
+ */
36
30
  @PostMapping("/checkout")
37
- public ResponseEntity<?> createCheckout(@RequestBody Map<String, String> request) {
31
+ public ResponseEntity<?> createCheckout(
32
+ @AuthenticationPrincipal UserDetails userDetails,
33
+ @RequestBody Map<String, String> body) {
38
34
  try {
39
- String customerId = request.get("customerId"); // Optional
40
- com.stripe.model.checkout.Session session = stripeService.createCheckoutSession(customerId);
35
+ String priceId = body.get("priceId");
36
+ if (priceId == null || priceId.isEmpty()) {
37
+ return ResponseEntity.badRequest().body(Map.of("error", "priceId is required"));
38
+ }
39
+ // userDetails.getUsername() returns the user ID (set in JWT filter)
40
+ com.stripe.model.checkout.Session session =
41
+ stripeService.createCheckoutSession(userDetails.getUsername(), priceId);
41
42
  return ResponseEntity.ok(Map.of("url", session.getUrl()));
42
43
  } catch (Exception e) {
43
44
  logger.error("Error creating checkout session", e);
44
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
45
- .body(Map.of("error", e.getMessage()));
45
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
46
+ }
47
+ }
48
+
49
+ /**
50
+ * POST /api/payments/portal
51
+ * Requires authentication. Opens Stripe Billing Portal.
52
+ */
53
+ @PostMapping("/portal")
54
+ public ResponseEntity<?> createPortal(@AuthenticationPrincipal UserDetails userDetails) {
55
+ try {
56
+ com.stripe.model.billingportal.Session session =
57
+ stripeService.createPortalSession(userDetails.getUsername());
58
+ return ResponseEntity.ok(Map.of("url", session.getUrl()));
59
+ } catch (Exception e) {
60
+ logger.error("Error creating portal session", e);
61
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
46
62
  }
47
63
  }
48
64
 
65
+ /**
66
+ * POST /api/payments/webhook
67
+ * No authentication. Stripe sends raw body + signature header.
68
+ */
49
69
  @PostMapping("/webhook")
50
70
  public ResponseEntity<String> handleWebhook(
51
71
  @RequestBody String payload,
52
72
  @RequestHeader("Stripe-Signature") String sigHeader) {
53
-
54
- Event event;
55
-
56
73
  try {
57
- event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
58
- } catch (SignatureVerificationException e) {
59
- logger.warn("Invalid signature", e);
60
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid signature");
74
+ stripeService.handleWebhook(payload, sigHeader);
75
+ return ResponseEntity.ok("{\"received\": true}");
61
76
  } catch (Exception e) {
62
- logger.error("Error parsing webhook", e);
63
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Error parsing webhook");
77
+ logger.warn("Webhook error: {}", e.getMessage());
78
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
64
79
  }
65
-
66
- switch (event.getType()) {
67
- case "checkout.session.completed":
68
- com.stripe.model.checkout.Session session = (com.stripe.model.checkout.Session) event.getDataObjectDeserializer().getObject().orElse(null);
69
- if (session != null) {
70
- logger.info("Checkout completed: {}", session.getId());
71
- // Match to database user here using session properties
72
- }
73
- break;
74
- case "customer.subscription.updated":
75
- com.stripe.model.Subscription subscription = (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
76
- if (subscription != null) {
77
- logger.info("Subscription updated: {}", subscription.getId());
78
- }
79
- break;
80
- case "customer.subscription.deleted":
81
- com.stripe.model.Subscription deletedSub = (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
82
- if (deletedSub != null) {
83
- logger.info("Subscription deleted: {}", deletedSub.getId());
84
- }
85
- break;
86
- default:
87
- logger.info("Unhandled event type: {}", event.getType());
88
- }
89
-
90
- return ResponseEntity.ok("Received");
91
80
  }
92
81
  }
@@ -3,65 +3,147 @@ package {{packageName}}.service;
3
3
  import com.stripe.Stripe;
4
4
  import com.stripe.exception.StripeException;
5
5
  import com.stripe.model.Customer;
6
+ import com.stripe.model.Event;
6
7
  import com.stripe.model.checkout.Session;
8
+ import com.stripe.net.Webhook;
7
9
  import com.stripe.param.CustomerCreateParams;
8
10
  import com.stripe.param.checkout.SessionCreateParams;
11
+ import {{packageName}}.model.User;
12
+ import {{packageName}}.repository.UserRepository;
9
13
  import jakarta.annotation.PostConstruct;
14
+ import org.slf4j.Logger;
15
+ import org.slf4j.LoggerFactory;
10
16
  import org.springframework.beans.factory.annotation.Value;
11
17
  import org.springframework.stereotype.Service;
12
18
 
13
19
  @Service
14
20
  public class StripeService {
15
21
 
22
+ private static final Logger logger = LoggerFactory.getLogger(StripeService.class);
23
+
16
24
  @Value("${stripe.secret-key}")
17
25
  private String stripeSecretKey;
18
26
 
19
- @Value("${stripe.price-id}")
20
- private String priceId;
27
+ @Value("${stripe.webhook-secret}")
28
+ private String webhookSecret;
21
29
 
22
30
  @Value("${frontend.url}")
23
31
  private String frontendUrl;
24
32
 
33
+ private final UserRepository userRepository;
34
+
35
+ public StripeService(UserRepository userRepository) {
36
+ this.userRepository = userRepository;
37
+ }
38
+
25
39
  @PostConstruct
26
40
  public void init() {
27
41
  Stripe.apiKey = stripeSecretKey;
28
42
  }
29
43
 
30
- public Session createCheckoutSession(String customerId) throws StripeException {
31
- SessionCreateParams.Builder paramsBuilder = SessionCreateParams.builder()
44
+ /**
45
+ * Retrieve or create a Stripe customer for the given user, then open a checkout session.
46
+ */
47
+ public Session createCheckoutSession(String userId, String priceId) throws StripeException {
48
+ User user = userRepository.findById(java.util.UUID.fromString(userId))
49
+ .orElseThrow(() -> new RuntimeException("User not found"));
50
+
51
+ String customerId = user.getStripeCustomerId();
52
+
53
+ if (customerId == null || customerId.isEmpty()) {
54
+ CustomerCreateParams customerParams = CustomerCreateParams.builder()
55
+ .setEmail(user.getEmail())
56
+ .putMetadata("userId", userId)
57
+ .build();
58
+ Customer customer = Customer.create(customerParams);
59
+ customerId = customer.getId();
60
+ user.setStripeCustomerId(customerId);
61
+ userRepository.save(user);
62
+ }
63
+
64
+ SessionCreateParams params = SessionCreateParams.builder()
32
65
  .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
66
+ .addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD)
67
+ .addLineItem(SessionCreateParams.LineItem.builder()
68
+ .setPrice(priceId)
69
+ .setQuantity(1L)
70
+ .build())
71
+ .setCustomer(customerId)
33
72
  .setSuccessUrl(frontendUrl + "/success?session_id={CHECKOUT_SESSION_ID}")
34
73
  .setCancelUrl(frontendUrl + "/cancel")
35
- .addLineItem(
36
- SessionCreateParams.LineItem.builder()
37
- .setPrice(priceId)
38
- .setQuantity(1L)
39
- .build()
40
- );
41
-
42
- if (customerId != null && !customerId.isEmpty()) {
43
- paramsBuilder.setCustomer(customerId);
44
- }
74
+ .setClientReferenceId(userId)
75
+ .build();
45
76
 
46
- return Session.create(paramsBuilder.build());
77
+ return Session.create(params);
47
78
  }
48
79
 
49
- public com.stripe.model.billingportal.Session createPortalSession(String customerId) throws StripeException {
80
+ /**
81
+ * Open the Stripe Billing Portal for the given customer.
82
+ */
83
+ public com.stripe.model.billingportal.Session createPortalSession(String userId) throws StripeException {
84
+ User user = userRepository.findById(java.util.UUID.fromString(userId))
85
+ .orElseThrow(() -> new RuntimeException("User not found"));
86
+
87
+ if (user.getStripeCustomerId() == null) {
88
+ throw new RuntimeException("No Stripe customer found for this user");
89
+ }
90
+
50
91
  com.stripe.param.billingportal.SessionCreateParams params =
51
92
  com.stripe.param.billingportal.SessionCreateParams.builder()
52
- .setCustomer(customerId)
93
+ .setCustomer(user.getStripeCustomerId())
53
94
  .setReturnUrl(frontendUrl + "/dashboard")
54
95
  .build();
55
96
 
56
97
  return com.stripe.model.billingportal.Session.create(params);
57
98
  }
58
99
 
59
- public Customer createCustomer(String email, String name) throws StripeException {
60
- CustomerCreateParams params = CustomerCreateParams.builder()
61
- .setEmail(email)
62
- .setName(name)
63
- .build();
100
+ /**
101
+ * Validate and handle a Stripe webhook event.
102
+ */
103
+ public void handleWebhook(String payload, String sigHeader) throws Exception {
104
+ Event event;
105
+ try {
106
+ event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
107
+ } catch (Exception e) {
108
+ throw new RuntimeException("Webhook signature verification failed: " + e.getMessage());
109
+ }
64
110
 
65
- return Customer.create(params);
111
+ switch (event.getType()) {
112
+ case "checkout.session.completed": {
113
+ Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
114
+ if (session != null && session.getClientReferenceId() != null) {
115
+ String userId = session.getClientReferenceId();
116
+ userRepository.findById(java.util.UUID.fromString(userId)).ifPresent(user -> {
117
+ user.setStripeCustomerId(session.getCustomer());
118
+ userRepository.save(user);
119
+ });
120
+ logger.info("Checkout completed for user: {}", userId);
121
+ }
122
+ break;
123
+ }
124
+ case "customer.subscription.updated": {
125
+ com.stripe.model.Subscription sub =
126
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
127
+ if (sub != null) logger.info("Subscription updated: {} | Status: {}", sub.getId(), sub.getStatus());
128
+ // TODO: update subscriptionStatus field on user
129
+ break;
130
+ }
131
+ case "customer.subscription.deleted": {
132
+ com.stripe.model.Subscription sub =
133
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
134
+ if (sub != null) logger.info("Subscription deleted: {}", sub.getId());
135
+ // TODO: mark user as unsubscribed
136
+ break;
137
+ }
138
+ case "invoice.payment_failed": {
139
+ com.stripe.model.Invoice invoice =
140
+ (com.stripe.model.Invoice) event.getDataObjectDeserializer().getObject().orElse(null);
141
+ if (invoice != null) logger.info("Payment failed for invoice: {}", invoice.getId());
142
+ // TODO: notify user via email
143
+ break;
144
+ }
145
+ default:
146
+ logger.info("Unhandled Stripe event: {}", event.getType());
147
+ }
66
148
  }
67
149
  }
@@ -5,9 +5,13 @@ terraform {
5
5
 
6
6
  required_providers {
7
7
  aws = {
8
- source = "hashicorp/aws"
8
+ source = "hashicorp/aws"
9
9
  version = "~> 5.0"
10
10
  }
11
+ random = {
12
+ source = "hashicorp/random"
13
+ version = "~> 3.5"
14
+ }
11
15
  }
12
16
 
13
17
  # Uncomment for remote state (recommended for production)
@@ -27,27 +31,27 @@ provider "aws" {
27
31
  # Variables
28
32
  variable "aws_region" {
29
33
  description = "AWS region"
30
- type = string
31
- default = "us-east-1"
34
+ type = string
35
+ default = "us-east-1"
32
36
  }
33
37
 
34
38
  variable "environment" {
35
39
  description = "Environment name (dev, staging, prod)"
36
- type = string
37
- default = "dev"
40
+ type = string
41
+ default = "dev"
38
42
  }
39
43
 
40
44
  variable "app_name" {
41
45
  description = "Application name"
42
- type = string
43
- default = "{{projectNameKebabCase}}"
46
+ type = string
47
+ default = "{{projectNameKebabCase}}"
44
48
  }
45
49
 
46
50
  # VPC
47
51
  module "vpc" {
48
52
  source = "./modules/vpc"
49
53
 
50
- app_name = var.app_name
54
+ app_name = var.app_name
51
55
  environment = var.environment
52
56
  }
53
57
 
@@ -55,29 +59,49 @@ module "vpc" {
55
59
  module "ecs" {
56
60
  source = "./modules/ecs"
57
61
 
58
- app_name = var.app_name
59
- environment = var.environment
60
- vpc_id = module.vpc.vpc_id
61
- subnet_ids = module.vpc.private_subnet_ids
62
+ app_name = var.app_name
63
+ environment = var.environment
64
+ vpc_id = module.vpc.vpc_id
65
+ public_subnet_ids = module.vpc.public_subnet_ids
66
+ private_subnet_ids = module.vpc.private_subnet_ids
67
+ alb_security_group_id = module.vpc.alb_security_group_id
68
+ ecs_tasks_security_group_id = module.vpc.ecs_tasks_security_group_id
62
69
  }
63
70
 
64
71
  # RDS PostgreSQL
65
72
  module "rds" {
66
73
  source = "./modules/rds"
67
74
 
68
- app_name = var.app_name
69
- environment = var.environment
70
- vpc_id = module.vpc.vpc_id
71
- subnet_ids = module.vpc.private_subnet_ids
75
+ app_name = var.app_name
76
+ environment = var.environment
77
+ vpc_id = module.vpc.vpc_id
78
+ subnet_ids = module.vpc.private_subnet_ids
72
79
  security_group_id = module.vpc.db_security_group_id
73
80
  }
74
81
 
75
82
  # Outputs
83
+ output "vpc_id" {
84
+ value = module.vpc.vpc_id
85
+ }
86
+
76
87
  output "ecs_cluster_name" {
77
88
  value = module.ecs.cluster_name
78
89
  }
79
90
 
91
+ output "ecr_repository_url" {
92
+ value = module.ecs.ecr_repository_url
93
+ }
94
+
95
+ output "alb_dns_name" {
96
+ value = module.ecs.alb_dns_name
97
+ description = "The DNS name of the ALB to access the application"
98
+ }
99
+
80
100
  output "rds_endpoint" {
81
- value = module.rds.endpoint
101
+ value = module.rds.endpoint
82
102
  sensitive = true
83
- }
103
+ }
104
+
105
+ output "db_name" {
106
+ value = module.rds.db_name
107
+ }
@@ -12,21 +12,54 @@ variable "vpc_id" {
12
12
  type = string
13
13
  }
14
14
 
15
- variable "subnet_ids" {
15
+ variable "public_subnet_ids" {
16
16
  type = list(string)
17
17
  }
18
18
 
19
+ variable "private_subnet_ids" {
20
+ type = list(string)
21
+ }
22
+
23
+ variable "alb_security_group_id" {
24
+ type = string
25
+ }
26
+
27
+ variable "ecs_tasks_security_group_id" {
28
+ type = string
29
+ }
30
+
31
+ variable "container_port" {
32
+ type = number
33
+ default = 3000
34
+ }
35
+
36
+ # ECR Repository
37
+ resource "aws_ecr_repository" "app" {
38
+ name = "${var.app_name}-${var.environment}"
39
+ image_tag_mutability = "MUTABLE"
40
+ force_delete = true
41
+
42
+ image_scanning_configuration {
43
+ scan_on_push = true
44
+ }
45
+
46
+ tags = {
47
+ Name = "${var.app_name}-${var.environment}"
48
+ Environment = var.environment
49
+ }
50
+ }
51
+
19
52
  # ECS Cluster
20
53
  resource "aws_ecs_cluster" "main" {
21
54
  name = "${var.app_name}-${var.environment}"
22
55
 
23
56
  setting {
24
- name = "containerInsights"
57
+ name = "containerInsights"
25
58
  value = "enabled"
26
59
  }
27
60
 
28
61
  tags = {
29
- Name = "${var.app_name}-${var.environment}"
62
+ Name = "${var.app_name}-${var.environment}"
30
63
  Environment = var.environment
31
64
  }
32
65
  }
@@ -38,12 +71,182 @@ resource "aws_ecs_cluster_capacity_providers" "main" {
38
71
  capacity_providers = ["FARGATE", "FARGATE_SPOT"]
39
72
 
40
73
  default_capacity_provider_strategy {
41
- base = 1
42
- weight = 100
74
+ base = 1
75
+ weight = 100
43
76
  capacity_provider = "FARGATE"
44
77
  }
45
78
  }
46
79
 
80
+ # Application Load Balancer
81
+ resource "aws_lb" "main" {
82
+ name = "${var.app_name}-${var.environment}-alb"
83
+ internal = false
84
+ load_balancer_type = "application"
85
+ security_groups = [var.alb_security_group_id]
86
+ subnets = var.public_subnet_ids
87
+
88
+ enable_deletion_protection = false
89
+
90
+ tags = {
91
+ Name = "${var.app_name}-${var.environment}-alb"
92
+ Environment = var.environment
93
+ }
94
+ }
95
+
96
+ # ALB Target Group
97
+ resource "aws_lb_target_group" "app" {
98
+ name = "${var.app_name}-${var.environment}-tg"
99
+ port = var.container_port
100
+ protocol = "HTTP"
101
+ vpc_id = var.vpc_id
102
+ target_type = "ip"
103
+
104
+ health_check {
105
+ path = "/health"
106
+ healthy_threshold = 2
107
+ unhealthy_threshold = 10
108
+ timeout = 30
109
+ interval = 40
110
+ matcher = "200-399"
111
+ }
112
+
113
+ tags = {
114
+ Name = "${var.app_name}-${var.environment}-tg"
115
+ Environment = var.environment
116
+ }
117
+ }
118
+
119
+ # ALB Listener (HTTP)
120
+ resource "aws_lb_listener" "http" {
121
+ load_balancer_arn = aws_lb.main.arn
122
+ port = "80"
123
+ protocol = "HTTP"
124
+
125
+ default_action {
126
+ type = "forward"
127
+ target_group_arn = aws_lb_target_group.app.arn
128
+ }
129
+ }
130
+
131
+ # CloudWatch Log Group for ECS
132
+ resource "aws_cloudwatch_log_group" "ecs" {
133
+ name = "/ecs/${var.app_name}-${var.environment}"
134
+ retention_in_days = 14
135
+
136
+ tags = {
137
+ Environment = var.environment
138
+ }
139
+ }
140
+
141
+ # IAM Role for ECS Task Execution
142
+ resource "aws_iam_role" "ecs_task_execution_role" {
143
+ name = "${var.app_name}-${var.environment}-ecs-execution-role"
144
+
145
+ assume_role_policy = jsonencode({
146
+ Version = "2012-10-17"
147
+ Statement = [
148
+ {
149
+ Action = "sts:AssumeRole"
150
+ Effect = "Allow"
151
+ Principal = {
152
+ Service = "ecs-tasks.amazonaws.com"
153
+ }
154
+ }
155
+ ]
156
+ })
157
+ }
158
+
159
+ resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" {
160
+ role = aws_iam_role.ecs_task_execution_role.name
161
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
162
+ }
163
+
164
+ # IAM Role for ECS Task (the application itself)
165
+ resource "aws_iam_role" "ecs_task_role" {
166
+ name = "${var.app_name}-${var.environment}-ecs-task-role"
167
+
168
+ assume_role_policy = jsonencode({
169
+ Version = "2012-10-17"
170
+ Statement = [
171
+ {
172
+ Action = "sts:AssumeRole"
173
+ Effect = "Allow"
174
+ Principal = {
175
+ Service = "ecs-tasks.amazonaws.com"
176
+ }
177
+ }
178
+ ]
179
+ })
180
+ }
181
+
182
+ # ECS Task Definition
183
+ resource "aws_ecs_task_definition" "app" {
184
+ family = "${var.app_name}-${var.environment}"
185
+ requires_compatibilities = ["FARGATE"]
186
+ network_mode = "awsvpc"
187
+ cpu = 256
188
+ memory = 512
189
+ execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
190
+ task_role_arn = aws_iam_role.ecs_task_role.arn
191
+
192
+ container_definitions = jsonencode([
193
+ {
194
+ name = "${var.app_name}"
195
+ image = "${aws_ecr_repository.app.repository_url}:latest"
196
+ essential = true
197
+
198
+ portMappings = [
199
+ {
200
+ containerPort = var.container_port
201
+ hostPort = var.container_port
202
+ protocol = "tcp"
203
+ }
204
+ ]
205
+
206
+ environment = [
207
+ { name = "NODE_ENV", value = var.environment == "prod" ? "production" : "development" },
208
+ { name = "PORT", value = tostring(var.container_port) }
209
+ ]
210
+
211
+ # Note: Add Secrets (like DB passwords) via SSM Parameter Store dynamically in a real deployment
212
+
213
+ logConfiguration = {
214
+ logDriver = "awslogs"
215
+ options = {
216
+ "awslogs-group" = aws_cloudwatch_log_group.ecs.name
217
+ "awslogs-region" = "us-east-1"
218
+ "awslogs-stream-prefix" = "ecs"
219
+ }
220
+ }
221
+ }
222
+ ])
223
+ }
224
+
225
+ # ECS Service
226
+ resource "aws_ecs_service" "app" {
227
+ name = "${var.app_name}-${var.environment}"
228
+ cluster = aws_ecs_cluster.main.id
229
+ task_definition = aws_ecs_task_definition.app.arn
230
+ desired_count = 1
231
+ launch_type = "FARGATE"
232
+
233
+ network_configuration {
234
+ subnets = var.private_subnet_ids
235
+ security_groups = [var.ecs_tasks_security_group_id]
236
+ assign_public_ip = false
237
+ }
238
+
239
+ load_balancer {
240
+ target_group_arn = aws_lb_target_group.app.arn
241
+ container_name = "${var.app_name}"
242
+ container_port = var.container_port
243
+ }
244
+
245
+ depends_on = [
246
+ aws_lb_listener.http
247
+ ]
248
+ }
249
+
47
250
  # Outputs
48
251
  output "cluster_name" {
49
252
  value = aws_ecs_cluster.main.name
@@ -51,4 +254,12 @@ output "cluster_name" {
51
254
 
52
255
  output "cluster_arn" {
53
256
  value = aws_ecs_cluster.main.arn
54
- }
257
+ }
258
+
259
+ output "ecr_repository_url" {
260
+ value = aws_ecr_repository.app.repository_url
261
+ }
262
+
263
+ output "alb_dns_name" {
264
+ value = aws_lb.main.dns_name
265
+ }