@aws/nx-plugin 1.0.0-rc.83 → 1.0.0-rc.85

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 (104) hide show
  1. package/generators.json +7 -0
  2. package/migrations.json +31 -1
  3. package/package.json +1 -1
  4. package/src/agentcore-gateway/__snapshots__/generator.spec.ts.snap +51 -2
  5. package/src/agentcore-harness/__snapshots__/generator.spec.ts.snap +4 -5
  6. package/src/agentcore-harness/files/project/README.md.template +4 -5
  7. package/src/internal/test-matrix/generator.js +16 -0
  8. package/src/internal/test-matrix/generator.js.map +1 -1
  9. package/src/migrations/latest/agentcore-harness-no-tools-by-default/__snapshots__/migration.spec.ts.snap +79 -0
  10. package/src/migrations/latest/agentcore-harness-no-tools-by-default/metadata.json +3 -0
  11. package/src/migrations/latest/agentcore-harness-no-tools-by-default/migration.d.ts +6 -0
  12. package/src/migrations/latest/agentcore-harness-no-tools-by-default/migration.js +87 -0
  13. package/src/migrations/latest/agentcore-harness-no-tools-by-default/migration.js.map +1 -0
  14. package/src/migrations/latest/rdb-terraform-aurora-app-module-defaults/__snapshots__/migration.spec.ts.snap +54 -0
  15. package/src/migrations/latest/rdb-terraform-aurora-app-module-defaults/metadata.json +3 -0
  16. package/src/migrations/latest/rdb-terraform-aurora-app-module-defaults/migration.d.ts +6 -0
  17. package/src/migrations/latest/rdb-terraform-aurora-app-module-defaults/migration.js +86 -0
  18. package/src/migrations/latest/rdb-terraform-aurora-app-module-defaults/migration.js.map +1 -0
  19. package/src/migrations/latest/terraform-declare-used-providers/metadata.json +3 -0
  20. package/src/migrations/latest/terraform-declare-used-providers/migration.d.ts +6 -0
  21. package/src/migrations/latest/terraform-declare-used-providers/migration.js +147 -0
  22. package/src/migrations/latest/terraform-declare-used-providers/migration.js.map +1 -0
  23. package/src/migrations/latest/terraform-harness-vpc-role-and-memory-grant/__snapshots__/migration.spec.ts.snap +521 -0
  24. package/src/migrations/latest/terraform-harness-vpc-role-and-memory-grant/metadata.json +3 -0
  25. package/src/migrations/latest/terraform-harness-vpc-role-and-memory-grant/migration.d.ts +6 -0
  26. package/src/migrations/latest/terraform-harness-vpc-role-and-memory-grant/migration.js +504 -0
  27. package/src/migrations/latest/terraform-harness-vpc-role-and-memory-grant/migration.js.map +1 -0
  28. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/metadata.json +3 -0
  29. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.d.ts +6 -0
  30. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js +146 -0
  31. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js.map +1 -0
  32. package/src/migrations/latest/waf-log-group-cmk-encryption/__snapshots__/migration.spec.ts.snap +286 -0
  33. package/src/migrations/latest/waf-log-group-cmk-encryption/metadata.json +3 -0
  34. package/src/migrations/latest/waf-log-group-cmk-encryption/migration.d.ts +6 -0
  35. package/src/migrations/latest/waf-log-group-cmk-encryption/migration.js +238 -0
  36. package/src/migrations/latest/waf-log-group-cmk-encryption/migration.js.map +1 -0
  37. package/src/open-api/json-metadata/generator.d.ts +24 -0
  38. package/src/open-api/json-metadata/generator.js +37 -0
  39. package/src/open-api/json-metadata/generator.js.map +1 -0
  40. package/src/open-api/json-metadata/schema.d.js +6 -0
  41. package/src/open-api/json-metadata/schema.d.js.map +1 -0
  42. package/src/open-api/json-metadata/schema.d.ts +9 -0
  43. package/src/open-api/json-metadata/schema.json +20 -0
  44. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +12 -0
  45. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +1875 -623
  46. package/src/py/fast-api/generator.js +2 -1
  47. package/src/py/fast-api/generator.js.map +1 -1
  48. package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +8 -0
  49. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +4 -0
  50. package/src/py/rdb/__snapshots__/generator.spec.ts.snap +31 -2
  51. package/src/sdk/open-api.d.ts +2 -0
  52. package/src/sdk/open-api.js +2 -1
  53. package/src/sdk/open-api.js.map +1 -1
  54. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +1756 -457
  55. package/src/smithy/ts/api/generator.js +2 -1
  56. package/src/smithy/ts/api/generator.js.map +1 -1
  57. package/src/terraform/project/files/application/bootstrap/providers.tf.template +11 -0
  58. package/src/terraform/project/files/application/scripts/bootstrap-destroy.ts.template +111 -0
  59. package/src/terraform/project/files/application/src/providers.tf.template +6 -0
  60. package/src/terraform/project/files/checkov/checkov.yml.template +8 -0
  61. package/src/terraform/project/files/library/src/main.tf.template +9 -0
  62. package/src/terraform/project/generator.js +47 -4
  63. package/src/terraform/project/generator.js.map +1 -1
  64. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +1848 -614
  65. package/src/trpc/backend/files-operations/scripts/generate-operations.ts.template +40 -0
  66. package/src/trpc/backend/generator.js +10 -0
  67. package/src/trpc/backend/generator.js.map +1 -1
  68. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +8 -0
  69. package/src/ts/dcr-proxy/__snapshots__/generator.spec.ts.snap +4 -0
  70. package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +8 -0
  71. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +4 -0
  72. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +62 -4
  73. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +4 -0
  74. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +35 -8
  75. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +4 -0
  76. package/src/utils/agent-core-constructs/files/cdk/app/agentcore-harness/__nameKebabCase__/__nameKebabCase__.ts.template +39 -1
  77. package/src/utils/agent-core-constructs/files/terraform/app/agent-core/__nameKebabCase__/__nameKebabCase__.tf.template +8 -1
  78. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.tf.template +57 -2
  79. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-harness/__nameKebabCase__/__nameKebabCase__.tf.template +258 -24
  80. package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +4 -0
  81. package/src/utils/api-constructs/api-constructs.js +11 -0
  82. package/src/utils/api-constructs/api-constructs.js.map +1 -1
  83. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +251 -3
  84. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +437 -2
  85. package/src/utils/api-constructs/files/terraform/core/api/rest/rest-api/rest-api.tf.template +60 -7
  86. package/src/utils/api-constructs/open-api-metadata.d.ts +20 -3
  87. package/src/utils/api-constructs/open-api-metadata.js +64 -8
  88. package/src/utils/api-constructs/open-api-metadata.js.map +1 -1
  89. package/src/utils/api-constructs/trpc-operations-metadata.d.ts +21 -0
  90. package/src/utils/api-constructs/trpc-operations-metadata.js +55 -0
  91. package/src/utils/api-constructs/trpc-operations-metadata.js.map +1 -0
  92. package/src/utils/dcr-proxy-constructs/files/terraform/app/dcr-proxies/__nameKebabCase__/__nameKebabCase__.tf.template +4 -0
  93. package/src/utils/files/terraform/src/metrics/providers.tf.template +12 -0
  94. package/src/utils/function-constructs/files/terraform/app/lambda-functions/__nameKebabCase__/__nameKebabCase__.tf.template +4 -0
  95. package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +33 -8
  96. package/src/utils/identity-constructs/files/terraform/core/user-identity/add-callback-url/add-callback-url.tf.template +4 -0
  97. package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +51 -2
  98. package/src/utils/rdb-constructs/files/cdk/core/rdb/aurora.ts.template +18 -0
  99. package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +15 -4
  100. package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +2 -2
  101. package/src/utils/rdb-constructs/rdb-constructs.d.ts +12 -0
  102. package/src/utils/rdb-constructs/rdb-constructs.js +24 -3
  103. package/src/utils/rdb-constructs/rdb-constructs.js.map +1 -1
  104. package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +4 -0
@@ -2,10 +2,18 @@ terraform {
2
2
  required_version = ">= 1.0"
3
3
 
4
4
  required_providers {
5
+ archive = {
6
+ source = "hashicorp/archive"
7
+ version = "<%- archiveProviderVersion %>"
8
+ }
5
9
  aws = {
6
10
  source = "hashicorp/aws"
7
11
  version = "<%- awsProviderVersion %>"
8
12
  }
13
+ random = {
14
+ source = "hashicorp/random"
15
+ version = "<%- randomProviderVersion %>"
16
+ }
9
17
  }
10
18
  }
11
19
 
@@ -123,6 +131,95 @@ resource "random_string" "suffix" {
123
131
  upper = false
124
132
  }
125
133
 
134
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
135
+ locals {
136
+ # Generated at build time from the API definition
137
+ operations_file = "${path.module}/../../../generated/<%- apiNameKebabCase %>/operations.json"
138
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
139
+
140
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
141
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
142
+
143
+ function_name = { for op in keys(local.operations) : op =>
144
+ "${substr("<%- apiNameClassName %>-${local.operation_slug[op]}", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
145
+ }
146
+ role_name = { for op in keys(local.operations) : op =>
147
+ "${substr("<%- apiNameClassName %>-${local.operation_slug[op]}-role", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
148
+ }
149
+
150
+ # A REST API needs a resource per path segment, so each operation's path is
151
+ # split into the distinct path prefixes the resource tree is built from below
152
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
153
+
154
+ path_nodes = merge([
155
+ for op, segments in local.operation_segments : {
156
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
157
+ depth = i
158
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
159
+ part = segment
160
+ }
161
+ }
162
+ ]...)
163
+
164
+ # Instances of a resource cannot reference one another, so each depth is a
165
+ # separate resource. Deeper paths are reported by the precondition below.
166
+ max_path_depth = <%- maxRestPathDepth %>
167
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
168
+ for path, node in local.path_nodes : path => node if node.depth == depth
169
+ } }
170
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
171
+
172
+ resource_ids = merge(
173
+ <%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
174
+ { for path, resource in aws_api_gateway_resource.path_depth_<%- depth %> : path => resource.id },
175
+ <%_ } _%>
176
+ )
177
+
178
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
179
+ local.resource_ids[join("/", segments)]
180
+ }
181
+
182
+ permission_source_suffix = { for op, details in local.operations : op =>
183
+ "${upper(details.method)}/${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
184
+ }
185
+ }
186
+
187
+ resource "terraform_data" "operations_metadata" {
188
+ # Fails the plan when the operations metadata has not been generated
189
+ input = local.operations_file
190
+
191
+ lifecycle {
192
+ precondition {
193
+ condition = fileexists(local.operations_file)
194
+ error_message = "Operations metadata not found at ${local.operations_file}. Build the <%- apiNameClassName %> project to generate it."
195
+ }
196
+ precondition {
197
+ condition = length(local.operations) > 0
198
+ error_message = "No operations found in ${local.operations_file}. The <%- apiNameClassName %> API must define at least one operation."
199
+ }
200
+ precondition {
201
+ condition = length(local.paths_too_deep) == 0
202
+ error_message = "Operation paths exceed the supported depth of ${local.max_path_depth} segments: ${join(", ", local.paths_too_deep)}. Add further aws_api_gateway_resource levels to this module to support them."
203
+ }
204
+ }
205
+ }
206
+
207
+ <%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
208
+ # API Gateway resources at path depth <%- depth %>
209
+ resource "aws_api_gateway_resource" "path_depth_<%- depth %>" {
210
+ for_each = local.path_nodes_by_depth[<%- depth %>]
211
+
212
+ rest_api_id = module.rest_api.api_id
213
+ <%_ if (depth === 0) { _%>
214
+ parent_id = module.rest_api.api_root_resource_id
215
+ <%_ } else { _%>
216
+ parent_id = aws_api_gateway_resource.path_depth_<%- depth - 1 %>[each.value.parent].id
217
+ <%_ } _%>
218
+ path_part = each.value.part
219
+ }
220
+
221
+ <%_ } _%>
222
+ <%_ } _%>
126
223
  # Resources
127
224
 
128
225
  # Create Lambda ZIP file from the bundle directory
@@ -197,8 +294,14 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
197
294
  description = "Allow outbound HTTPS to AWS service endpoints"
198
295
  }
199
296
 
200
- # Lambda function
201
297
  resource "aws_lambda_function" "api_lambda" {
298
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
299
+ # One lambda function per operation, each serving just that operation
300
+ for_each = local.operations
301
+
302
+ <%_ } else { _%>
303
+ # A single "router" lambda serving all requests
304
+ <%_ } _%>
202
305
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
203
306
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
204
307
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -207,8 +310,13 @@ resource "aws_lambda_function" "api_lambda" {
207
310
  s3_bucket = aws_s3_object.lambda_zip.bucket
208
311
  s3_key = aws_s3_object.lambda_zip.key
209
312
  s3_object_version = aws_s3_object.lambda_zip.version_id
313
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
314
+ function_name = local.function_name[each.key]
315
+ role = aws_iam_role.lambda_execution_role[each.key].arn
316
+ <%_ } else { _%>
210
317
  function_name = "<%- apiNameClassName %>Handler-${random_string.suffix.result}"
211
318
  role = aws_iam_role.lambda_execution_role.arn
319
+ <%_ } _%>
212
320
  <%_ if (['trpc', 'smithy'].includes(backend.type)) { _%>
213
321
  handler = "index.handler"
214
322
  runtime = "nodejs22.x"
@@ -264,7 +372,13 @@ resource "aws_lambda_function" "api_lambda" {
264
372
 
265
373
  # IAM role for Lambda execution
266
374
  resource "aws_iam_role" "lambda_execution_role" {
375
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
376
+ for_each = local.operations
377
+
378
+ name = local.role_name[each.key]
379
+ <%_ } else { _%>
267
380
  name = "<%- apiNameClassName %>Handler-execution-role-${random_string.suffix.result}"
381
+ <%_ } _%>
268
382
 
269
383
  assume_role_policy = jsonencode({
270
384
  Version = "2012-10-17"
@@ -284,25 +398,57 @@ resource "aws_iam_role" "lambda_execution_role" {
284
398
 
285
399
  # Attach basic execution policy to Lambda role
286
400
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
401
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
402
+ for_each = local.operations
403
+
404
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
405
+ role = aws_iam_role.lambda_execution_role[each.key].name
406
+ <%_ } else { _%>
287
407
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
288
408
  role = aws_iam_role.lambda_execution_role.name
409
+ <%_ } _%>
289
410
  }
290
411
 
291
412
  # Attach X-Ray tracing policy to Lambda role
292
413
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
414
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
415
+ for_each = local.operations
416
+
417
+ policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
418
+ role = aws_iam_role.lambda_execution_role[each.key].name
419
+ <%_ } else { _%>
293
420
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
294
421
  role = aws_iam_role.lambda_execution_role.name
422
+ <%_ } _%>
295
423
  }
296
424
 
297
425
  # Attach VPC access policy to Lambda role when deployed into a VPC
298
426
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
427
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
428
+ for_each = var.enable_vpc ? local.operations : {}
429
+
430
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
431
+ role = aws_iam_role.lambda_execution_role[each.key].name
432
+ <%_ } else { _%>
299
433
  count = var.enable_vpc ? 1 : 0
300
434
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
301
435
  role = aws_iam_role.lambda_execution_role.name
436
+ <%_ } _%>
302
437
  }
303
438
 
304
439
  # Additional IAM policies for Lambda (if provided)
305
440
  resource "aws_iam_role_policy" "lambda_additional_policies" {
441
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
442
+ for_each = local.operations
443
+
444
+ name = "${substr(local.function_name[each.key], 0, 55)}-policies"
445
+ role = aws_iam_role.lambda_execution_role[each.key].id
446
+
447
+ policy = jsonencode({
448
+ Version = "2012-10-17"
449
+ Statement = local.lambda_policy_document_statements
450
+ })
451
+ <%_ } else { _%>
306
452
  count = length(local.lambda_policy_statements) > 0 ? 1 : 0
307
453
  name = "<%- apiNameClassName %>Handler-additional-policies-${random_string.suffix.result}"
308
454
  role = aws_iam_role.lambda_execution_role.id
@@ -311,6 +457,7 @@ resource "aws_iam_role_policy" "lambda_additional_policies" {
311
457
  Version = "2012-10-17"
312
458
  Statement = local.lambda_policy_statements
313
459
  })
460
+ <%_ } _%>
314
461
  }
315
462
 
316
463
  locals {
@@ -325,6 +472,17 @@ locals {
325
472
  Resource = ["${var.appconfig_application_arn}/*"]
326
473
  }
327
474
  ] : [], var.additional_iam_policy_statements)
475
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
476
+
477
+ # IAM rejects an empty statement list, so fall back to a no-op deny
478
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
479
+ {
480
+ Effect = "Deny"
481
+ Action = ["appconfig:GetLatestConfiguration"]
482
+ Resource = ["arn:aws:appconfig:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:application/none"]
483
+ }
484
+ ]
485
+ <%_ } _%>
328
486
  }
329
487
 
330
488
  # CloudWatch Log Group for Lambda
@@ -332,16 +490,31 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
332
490
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
333
491
  #checkov:skip=CKV_AWS_338:Log retention set to forever
334
492
  #checkov:skip=CKV_AWS_66:Log retention set to forever
493
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
494
+ for_each = local.operations
495
+
496
+ name = "/aws/lambda/${local.function_name[each.key]}"
497
+ tags = var.tags
498
+ <%_ } else { _%>
335
499
  name = "/aws/lambda/<%- apiNameClassName %>Handler-${random_string.suffix.result}"
336
500
  tags = var.tags
501
+ <%_ } _%>
337
502
  }
338
503
 
339
504
  <%_ if (backend.type === 'fastapi') { _%>
340
505
  # Lambda alias pointing to the latest published version for SnapStart
341
506
  resource "aws_lambda_alias" "live" {
507
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
508
+ for_each = local.operations
509
+
510
+ name = "live"
511
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
512
+ function_version = aws_lambda_function.api_lambda[each.key].version
513
+ <%_ } else { _%>
342
514
  name = "live"
343
515
  function_name = aws_lambda_function.api_lambda.function_name
344
516
  function_version = aws_lambda_function.api_lambda.version
517
+ <%_ } _%>
345
518
 
346
519
  depends_on = [aws_lambda_function.api_lambda]
347
520
  }
@@ -464,6 +637,161 @@ resource "aws_lambda_permission" "authorizer_invoke" {
464
637
  }
465
638
  <%_ } _%>
466
639
 
640
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
641
+ # Method per operation
642
+ resource "aws_api_gateway_method" "operation_methods" {
643
+ #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
644
+ for_each = local.operations
645
+
646
+ rest_api_id = module.rest_api.api_id
647
+ resource_id = local.operation_resource_id[each.key]
648
+ http_method = upper(each.value.method)
649
+
650
+ <%_ if (auth === 'iam') { _%>
651
+ authorization = "AWS_IAM"
652
+ <%_ } else if (auth === 'cognito') { _%>
653
+ authorization = "COGNITO_USER_POOLS"
654
+ authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
655
+ # Accept access tokens from both sign-in flows: 'openid' (Cognito hosted UI)
656
+ # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
657
+ authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
658
+ <%_ } else if (auth === 'custom') { _%>
659
+ authorization = "CUSTOM"
660
+ authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
661
+ <%_ } _%>
662
+
663
+ # Path parameters must be declared on the method
664
+ request_parameters = {
665
+ for segment in local.operation_segments[each.key] :
666
+ "method.request.path.${trimsuffix(trimprefix(segment, "{"), "}")}" => true
667
+ if startswith(segment, "{")
668
+ }
669
+
670
+ depends_on = [<% if (auth === 'cognito') { %>aws_api_gateway_authorizer.cognito_authorizer<% } else if (auth === 'custom') { %>aws_api_gateway_authorizer.custom_authorizer<% } %>]
671
+ }
672
+
673
+ # Lambda integration per operation
674
+ resource "aws_api_gateway_integration" "lambda_integration" {
675
+ for_each = local.operations
676
+
677
+ rest_api_id = module.rest_api.api_id
678
+ resource_id = local.operation_resource_id[each.key]
679
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
680
+
681
+ integration_http_method = "POST"
682
+ type = "AWS_PROXY"
683
+ <%_ if (backend.type === 'fastapi') { _%>
684
+ # Use the response streaming invocation path with the alias ARN for SnapStart
685
+ uri = "arn:aws:apigateway:${data.aws_region.current.region}:lambda:path/2021-11-15/functions/${aws_lambda_alias.live[each.key].arn}/response-streaming-invocations"
686
+ response_transfer_mode = "STREAM"
687
+ <%_ } else if (backend.type === 'trpc') { _%>
688
+ uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
689
+ response_transfer_mode = "STREAM"
690
+ <%_ } else { _%>
691
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
692
+ <%_ } _%>
693
+
694
+ <%_ if (backend.type === 'fastapi') { _%>
695
+ depends_on = [aws_lambda_alias.live]
696
+ <%_ } else { _%>
697
+ depends_on = [aws_lambda_function.api_lambda]
698
+ <%_ } _%>
699
+ }
700
+
701
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
702
+ resource "aws_api_gateway_method" "options_method" {
703
+ #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
704
+ #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
705
+ for_each = local.resource_ids
706
+
707
+ rest_api_id = module.rest_api.api_id
708
+ resource_id = each.value
709
+ http_method = "OPTIONS"
710
+ authorization = "NONE"
711
+ }
712
+
713
+ # CORS integration for OPTIONS methods
714
+ resource "aws_api_gateway_integration" "options_integration" {
715
+ for_each = local.resource_ids
716
+
717
+ rest_api_id = module.rest_api.api_id
718
+ resource_id = each.value
719
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
720
+
721
+ type = "MOCK"
722
+ request_templates = {
723
+ "application/json" = "{\"statusCode\": 204}"
724
+ }
725
+ }
726
+
727
+ # OPTIONS method responses
728
+ resource "aws_api_gateway_method_response" "options_response" {
729
+ for_each = local.resource_ids
730
+
731
+ rest_api_id = module.rest_api.api_id
732
+ resource_id = each.value
733
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
734
+ status_code = "204"
735
+
736
+ response_parameters = {
737
+ "method.response.header.Access-Control-Allow-Headers" = true
738
+ "method.response.header.Access-Control-Allow-Methods" = true
739
+ "method.response.header.Access-Control-Allow-Origin" = true
740
+ }
741
+ }
742
+
743
+ # OPTIONS integration responses
744
+ resource "aws_api_gateway_integration_response" "options_integration_response" {
745
+ for_each = local.resource_ids
746
+
747
+ rest_api_id = module.rest_api.api_id
748
+ resource_id = each.value
749
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
750
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
751
+
752
+ response_parameters = {
753
+ "method.response.header.Access-Control-Allow-Headers" = "'${join(",", var.cors_allow_headers)}'"
754
+ "method.response.header.Access-Control-Allow-Methods" = "'${join(",", var.cors_allow_methods)}'"
755
+ "method.response.header.Access-Control-Allow-Origin" = "'${join(",", var.cors_allow_origins)}'"
756
+ }
757
+
758
+ depends_on = [aws_api_gateway_integration.options_integration]
759
+ }
760
+
761
+ # API Gateway deployment
762
+ resource "aws_api_gateway_deployment" "api_deployment" {
763
+ rest_api_id = module.rest_api.api_id
764
+
765
+ triggers = {
766
+ redeployment = sha1(jsonencode(concat(
767
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
768
+ <%_ for (let depth = 1; depth < maxRestPathDepth; depth++) { _%>
769
+ [for r in aws_api_gateway_resource.path_depth_<%- depth %> : r.id],
770
+ <%_ } _%>
771
+ [for m in aws_api_gateway_method.operation_methods : m.id],
772
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
773
+ [for m in aws_api_gateway_method.options_method : m.id],
774
+ [for i in aws_api_gateway_integration.options_integration : i.id],
775
+ )))
776
+ }
777
+
778
+ lifecycle {
779
+ create_before_destroy = true
780
+ }
781
+
782
+ depends_on = [
783
+ aws_api_gateway_method.operation_methods,
784
+ aws_api_gateway_integration.lambda_integration,
785
+ aws_api_gateway_method.options_method,
786
+ aws_api_gateway_integration.options_integration,
787
+ aws_api_gateway_method_response.options_response,
788
+ aws_api_gateway_integration_response.options_integration_response,
789
+ <%_ if (auth === 'custom') { _%>
790
+ aws_api_gateway_authorizer.custom_authorizer,
791
+ <%_ } _%>
792
+ ]
793
+ }
794
+ <%_ } else { _%>
467
795
  # Create proxy resource (captures all paths)
468
796
  resource "aws_api_gateway_resource" "proxy_resource" {
469
797
  rest_api_id = module.rest_api.api_id
@@ -605,6 +933,8 @@ resource "aws_api_gateway_deployment" "api_deployment" {
605
933
  ]
606
934
  }
607
935
 
936
+ <%_ } _%>
937
+
608
938
  # KMS key for encrypting access logs at rest
609
939
  resource "aws_kms_key" "access_logs" {
610
940
  description = "<%- apiNameClassName %> API access log encryption"
@@ -749,6 +1079,18 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
749
1079
  # Lambda permission for API Gateway to invoke the function
750
1080
  <%_ } _%>
751
1081
  resource "aws_lambda_permission" "api_gateway_invoke" {
1082
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1083
+ for_each = local.operations
1084
+
1085
+ statement_id = "AllowExecutionFromAPIGateway"
1086
+ action = "lambda:InvokeFunction"
1087
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1088
+ <%_ if (backend.type === 'fastapi') { _%>
1089
+ qualifier = aws_lambda_alias.live[each.key].name
1090
+ <%_ } _%>
1091
+ principal = "apigateway.amazonaws.com"
1092
+ source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
1093
+ <%_ } else { _%>
752
1094
  statement_id = "AllowExecutionFromAPIGateway"
753
1095
  action = "lambda:InvokeFunction"
754
1096
  function_name = aws_lambda_function.api_lambda.function_name
@@ -757,6 +1099,7 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
757
1099
  <%_ } _%>
758
1100
  principal = "apigateway.amazonaws.com"
759
1101
  source_arn = "${module.rest_api.api_execution_arn}/*/*"
1102
+ <%_ } _%>
760
1103
 
761
1104
  <%_ if (backend.type === 'fastapi') { _%>
762
1105
  depends_on = [module.rest_api, aws_lambda_alias.live]
@@ -768,12 +1111,23 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
768
1111
  <%_ if (backend.type === 'fastapi') { _%>
769
1112
  # Lambda permission for API Gateway to invoke with response streaming via alias
770
1113
  resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
1114
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1115
+ for_each = local.operations
1116
+
1117
+ statement_id = "AllowStreamingFromAPIGateway"
1118
+ action = "lambda:InvokeFunctionWithResponseStream"
1119
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1120
+ qualifier = aws_lambda_alias.live[each.key].name
1121
+ principal = "apigateway.amazonaws.com"
1122
+ source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
1123
+ <%_ } else { _%>
771
1124
  statement_id = "AllowStreamingFromAPIGateway"
772
1125
  action = "lambda:InvokeFunctionWithResponseStream"
773
1126
  function_name = aws_lambda_function.api_lambda.function_name
774
1127
  qualifier = aws_lambda_alias.live.name
775
1128
  principal = "apigateway.amazonaws.com"
776
1129
  source_arn = "${module.rest_api.api_execution_arn}/*/*"
1130
+ <%_ } _%>
777
1131
 
778
1132
  depends_on = [module.rest_api, aws_lambda_alias.live]
779
1133
  }
@@ -785,7 +1139,7 @@ module "add_url_to_runtime_config" {
785
1139
 
786
1140
  namespace = "connection"
787
1141
  key = "apis"
788
- value = { "<%- apiNameClassName %>" = aws_api_gateway_stage.api_stage.invoke_url }
1142
+ value = { "<%- apiNameClassName %>" = "${aws_api_gateway_stage.api_stage.invoke_url}/" }
789
1143
 
790
1144
  depends_on = [aws_api_gateway_stage.api_stage]
791
1145
  }
@@ -838,6 +1192,86 @@ output "stage_id" {
838
1192
  value = aws_api_gateway_stage.api_stage.id
839
1193
  }
840
1194
 
1195
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1196
+ # Lambda Function Outputs, keyed by operation name
1197
+ output "operations" {
1198
+ description = "Names of the API operations, each with its own Lambda function"
1199
+ value = keys(local.operations)
1200
+ }
1201
+
1202
+ output "lambda_function_names" {
1203
+ description = "Name of each operation's Lambda function, keyed by operation name"
1204
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
1205
+ }
1206
+
1207
+ output "lambda_function_arns" {
1208
+ description = "ARN of each operation's Lambda function, keyed by operation name"
1209
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
1210
+ }
1211
+
1212
+ output "lambda_invoke_arns" {
1213
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
1214
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
1215
+ }
1216
+
1217
+ output "lambda_qualified_arns" {
1218
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
1219
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
1220
+ }
1221
+
1222
+ output "lambda_versions" {
1223
+ description = "Version of each operation's Lambda function, keyed by operation name"
1224
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
1225
+ }
1226
+
1227
+ output "lambda_source_code_hash" {
1228
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
1229
+ value = data.archive_file.lambda_zip.output_base64sha256
1230
+ }
1231
+
1232
+ # IAM Role Outputs, keyed by operation name
1233
+ output "lambda_execution_role_arns" {
1234
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
1235
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
1236
+ }
1237
+
1238
+ output "lambda_execution_role_names" {
1239
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
1240
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
1241
+ }
1242
+
1243
+ output "security_group_id" {
1244
+ description = "Security group ID shared by the API Lambda functions, for use in ingress rules on resources they must reach (e.g. a database). Null unless enable_vpc is true."
1245
+ value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
1246
+ }
1247
+
1248
+ # Integration Outputs, keyed by operation name
1249
+ output "integration_ids" {
1250
+ description = "ID of each operation's Lambda integration, keyed by operation name"
1251
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
1252
+ }
1253
+
1254
+ output "method_ids" {
1255
+ description = "ID of each operation's API Gateway method, keyed by operation name"
1256
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
1257
+ }
1258
+
1259
+ output "resource_ids" {
1260
+ description = "ID of each API Gateway resource, keyed by its path"
1261
+ value = local.resource_ids
1262
+ }
1263
+
1264
+ # CloudWatch Log Groups, keyed by operation name
1265
+ output "lambda_log_group_names" {
1266
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
1267
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
1268
+ }
1269
+
1270
+ output "lambda_log_group_arns" {
1271
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
1272
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
1273
+ }
1274
+ <%_ } else { _%>
841
1275
  # Lambda Function Outputs
842
1276
  output "lambda_function_name" {
843
1277
  description = "Name of the Lambda function"
@@ -916,3 +1350,4 @@ output "lambda_log_group_arn" {
916
1350
  description = "ARN of the Lambda CloudWatch log group"
917
1351
  value = aws_cloudwatch_log_group.lambda_logs.arn
918
1352
  }
1353
+ <%_ } _%>
@@ -200,22 +200,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
200
200
  }
201
201
  }
202
202
 
203
+ resource "random_id" "waf_log_suffix" {
204
+ count = var.enable_waf ? 1 : 0
205
+ byte_length = 4
206
+ }
207
+
208
+ locals {
209
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-${var.api_name}-${random_id.waf_log_suffix[0].hex}" : null
210
+ }
211
+
212
+ # KMS key for encrypting WAF request logs at rest
213
+ resource "aws_kms_key" "waf_logs" {
214
+ count = var.enable_waf ? 1 : 0
215
+
216
+ description = "${var.api_name} API WAF log encryption"
217
+ deletion_window_in_days = 7
218
+ enable_key_rotation = true
219
+
220
+ policy = jsonencode({
221
+ Version = "2012-10-17"
222
+ Statement = [
223
+ {
224
+ Sid = "Enable IAM User Permissions"
225
+ Effect = "Allow"
226
+ Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
227
+ Action = "kms:*"
228
+ Resource = "*"
229
+ },
230
+ {
231
+ Sid = "Allow CloudWatch Logs"
232
+ Effect = "Allow"
233
+ Principal = { Service = "logs.${data.aws_region.current.region}.amazonaws.com" }
234
+ Action = [
235
+ "kms:Encrypt",
236
+ "kms:Decrypt",
237
+ "kms:ReEncrypt*",
238
+ "kms:GenerateDataKey*",
239
+ "kms:DescribeKey"
240
+ ]
241
+ Resource = "*"
242
+ Condition = {
243
+ ArnEquals = {
244
+ "kms:EncryptionContext:aws:logs:arn" = "arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:${local.waf_log_group_name}"
245
+ }
246
+ }
247
+ }
248
+ ]
249
+ })
250
+
251
+ tags = var.tags
252
+ }
253
+
254
+ resource "aws_kms_alias" "waf_logs" {
255
+ count = var.enable_waf ? 1 : 0
256
+
257
+ name = "alias/${var.api_name}-${random_id.waf_log_suffix[0].hex}-waf-logs"
258
+ target_key_id = aws_kms_key.waf_logs[0].key_id
259
+ }
260
+
203
261
  # CloudWatch Log Group for WAF request logs. Name must start with `aws-waf-logs-`.
204
262
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
205
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
206
263
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
207
264
  count = var.enable_waf ? 1 : 0
208
265
 
209
- name = "aws-waf-logs-${var.api_name}-${random_id.waf_log_suffix[0].hex}"
266
+ name = local.waf_log_group_name
210
267
  retention_in_days = 30
268
+ kms_key_id = aws_kms_key.waf_logs[0].arn
211
269
  tags = var.tags
212
270
  }
213
271
 
214
- resource "random_id" "waf_log_suffix" {
215
- count = var.enable_waf ? 1 : 0
216
- byte_length = 4
217
- }
218
-
219
272
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
220
273
  count = var.enable_waf ? 1 : 0
221
274