@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
@@ -256,10 +256,18 @@ output "api_log_group_arn" {
256
256
  required_version = ">= 1.0"
257
257
 
258
258
  required_providers {
259
+ archive = {
260
+ source = "hashicorp/archive"
261
+ version = "2.8.0"
262
+ }
259
263
  aws = {
260
264
  source = "hashicorp/aws"
261
265
  version = "6.61.0"
262
266
  }
267
+ random = {
268
+ source = "hashicorp/random"
269
+ version = "3.9.0"
270
+ }
263
271
  }
264
272
  }
265
273
 
@@ -379,6 +387,49 @@ resource "random_string" "suffix" {
379
387
  upper = false
380
388
  }
381
389
 
390
+ locals {
391
+ # Generated at build time from the API definition
392
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
393
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
394
+
395
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
396
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
397
+
398
+ function_name = { for op in keys(local.operations) : op =>
399
+ "\${substr("TestApi-\${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}"
400
+ }
401
+ role_name = { for op in keys(local.operations) : op =>
402
+ "\${substr("TestApi-\${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}"
403
+ }
404
+
405
+ operation_path = { for op, details in local.operations : op =>
406
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
407
+ }
408
+ route_key = { for op, details in local.operations : op =>
409
+ "\${upper(details.method)} \${local.operation_path[op]}"
410
+ }
411
+
412
+ permission_source_suffix = { for op, details in local.operations : op =>
413
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
414
+ }
415
+ }
416
+
417
+ resource "terraform_data" "operations_metadata" {
418
+ # Fails the plan when the operations metadata has not been generated
419
+ input = local.operations_file
420
+
421
+ lifecycle {
422
+ precondition {
423
+ condition = fileexists(local.operations_file)
424
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
425
+ }
426
+ precondition {
427
+ condition = length(local.operations) > 0
428
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
429
+ }
430
+ }
431
+ }
432
+
382
433
  # Resources
383
434
 
384
435
  # Create Lambda ZIP file from the bundle directory
@@ -439,9 +490,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
439
490
  description = "Allow outbound HTTPS to AWS service endpoints"
440
491
  }
441
492
 
442
- # Lambda function
443
- # This configures a single "router" lambda to serve all requests
444
493
  resource "aws_lambda_function" "api_lambda" {
494
+ # One lambda function per operation, each serving just that operation
495
+ for_each = local.operations
496
+
445
497
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
446
498
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
447
499
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -450,8 +502,8 @@ resource "aws_lambda_function" "api_lambda" {
450
502
  s3_bucket = aws_s3_object.lambda_zip.bucket
451
503
  s3_key = aws_s3_object.lambda_zip.key
452
504
  s3_object_version = aws_s3_object.lambda_zip.version_id
453
- function_name = "TestApiHandler-\${random_string.suffix.result}"
454
- role = aws_iam_role.lambda_execution_role.arn
505
+ function_name = local.function_name[each.key]
506
+ role = aws_iam_role.lambda_execution_role[each.key].arn
455
507
  handler = "run.sh"
456
508
  runtime = "python3.14"
457
509
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -496,7 +548,9 @@ resource "aws_lambda_function" "api_lambda" {
496
548
 
497
549
  # IAM role for Lambda execution
498
550
  resource "aws_iam_role" "lambda_execution_role" {
499
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
551
+ for_each = local.operations
552
+
553
+ name = local.role_name[each.key]
500
554
 
501
555
  assume_role_policy = jsonencode({
502
556
  Version = "2012-10-17"
@@ -516,32 +570,38 @@ resource "aws_iam_role" "lambda_execution_role" {
516
570
 
517
571
  # Attach basic execution policy to Lambda role
518
572
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
573
+ for_each = local.operations
574
+
519
575
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
520
- role = aws_iam_role.lambda_execution_role.name
576
+ role = aws_iam_role.lambda_execution_role[each.key].name
521
577
  }
522
578
 
523
579
  # Attach X-Ray tracing policy to Lambda role
524
580
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
581
+ for_each = local.operations
582
+
525
583
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
526
- role = aws_iam_role.lambda_execution_role.name
584
+ role = aws_iam_role.lambda_execution_role[each.key].name
527
585
  }
528
586
 
529
587
  # Attach VPC access policy to Lambda role when deployed into a VPC
530
588
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
531
- count = var.enable_vpc ? 1 : 0
589
+ for_each = var.enable_vpc ? local.operations : {}
590
+
532
591
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
533
- role = aws_iam_role.lambda_execution_role.name
592
+ role = aws_iam_role.lambda_execution_role[each.key].name
534
593
  }
535
594
 
536
595
  # Additional IAM policies for Lambda (if provided)
537
596
  resource "aws_iam_role_policy" "lambda_additional_policies" {
538
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
539
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
540
- role = aws_iam_role.lambda_execution_role.id
597
+ for_each = local.operations
598
+
599
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
600
+ role = aws_iam_role.lambda_execution_role[each.key].id
541
601
 
542
602
  policy = jsonencode({
543
603
  Version = "2012-10-17"
544
- Statement = local.lambda_policy_statements
604
+ Statement = local.lambda_policy_document_statements
545
605
  })
546
606
  }
547
607
 
@@ -557,6 +617,15 @@ locals {
557
617
  Resource = ["\${var.appconfig_application_arn}/*"]
558
618
  }
559
619
  ] : [], var.additional_iam_policy_statements)
620
+
621
+ # IAM rejects an empty statement list, so fall back to a no-op deny
622
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
623
+ {
624
+ Effect = "Deny"
625
+ Action = ["appconfig:GetLatestConfiguration"]
626
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
627
+ }
628
+ ]
560
629
  }
561
630
 
562
631
  # CloudWatch Log Group for Lambda
@@ -564,15 +633,19 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
564
633
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
565
634
  #checkov:skip=CKV_AWS_338:Log retention set to forever
566
635
  #checkov:skip=CKV_AWS_66:Log retention set to forever
567
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
568
- tags = var.tags
636
+ for_each = local.operations
637
+
638
+ name = "/aws/lambda/\${local.function_name[each.key]}"
639
+ tags = var.tags
569
640
  }
570
641
 
571
642
  # Lambda alias pointing to the latest published version for SnapStart
572
643
  resource "aws_lambda_alias" "live" {
644
+ for_each = local.operations
645
+
573
646
  name = "live"
574
- function_name = aws_lambda_function.api_lambda.function_name
575
- function_version = aws_lambda_function.api_lambda.version
647
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
648
+ function_version = aws_lambda_function.api_lambda[each.key].version
576
649
 
577
650
  depends_on = [aws_lambda_function.api_lambda]
578
651
  }
@@ -592,9 +665,11 @@ resource "aws_apigatewayv2_authorizer" "cognito_authorizer" {
592
665
 
593
666
  # Lambda integration for HTTP API
594
667
  resource "aws_apigatewayv2_integration" "lambda_integration" {
668
+ for_each = local.operations
669
+
595
670
  api_id = module.http_api.api_id
596
671
  integration_type = "AWS_PROXY"
597
- integration_uri = aws_lambda_alias.live.invoke_arn
672
+ integration_uri = aws_lambda_alias.live[each.key].invoke_arn
598
673
 
599
674
  payload_format_version = "2.0"
600
675
  timeout_milliseconds = 30000
@@ -602,15 +677,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
602
677
  depends_on = [aws_lambda_alias.live]
603
678
  }
604
679
 
605
- # Route for proxy integration (catches all requests)
606
- resource "aws_apigatewayv2_route" "proxy_routes" {
607
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
608
- # when cors settings are configured
609
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
680
+ # One route per operation, each targeting that operation's integration
681
+ resource "aws_apigatewayv2_route" "operation_routes" {
682
+ for_each = local.operations
610
683
 
611
684
  api_id = module.http_api.api_id
612
- route_key = "\${each.key} /{proxy+}"
613
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
685
+ route_key = local.route_key[each.key]
686
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
614
687
 
615
688
  authorization_type = "JWT"
616
689
  authorizer_id = aws_apigatewayv2_authorizer.cognito_authorizer.id
@@ -631,12 +704,14 @@ module "add_url_to_runtime_config" {
631
704
 
632
705
  # Lambda permission for API Gateway to invoke the function via alias
633
706
  resource "aws_lambda_permission" "api_gateway_invoke" {
707
+ for_each = local.operations
708
+
634
709
  statement_id = "AllowExecutionFromAPIGateway"
635
710
  action = "lambda:InvokeFunction"
636
- function_name = aws_lambda_function.api_lambda.function_name
637
- qualifier = aws_lambda_alias.live.name
711
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
712
+ qualifier = aws_lambda_alias.live[each.key].name
638
713
  principal = "apigateway.amazonaws.com"
639
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
714
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
640
715
 
641
716
  depends_on = [module.http_api, aws_lambda_alias.live]
642
717
  }
@@ -679,73 +754,73 @@ output "stage_execution_arn" {
679
754
  value = module.http_api.stage_execution_arn
680
755
  }
681
756
 
682
- # Lambda Function Outputs
683
- output "lambda_function_name" {
684
- description = "Name of the Lambda function"
685
- value = aws_lambda_function.api_lambda.function_name
757
+ # Lambda Function Outputs, keyed by operation name
758
+ output "operations" {
759
+ description = "Names of the API operations, each with its own Lambda function"
760
+ value = keys(local.operations)
686
761
  }
687
762
 
688
- output "lambda_function_arn" {
689
- description = "ARN of the Lambda function"
690
- value = aws_lambda_function.api_lambda.arn
763
+ output "lambda_function_names" {
764
+ description = "Name of each operation's Lambda function, keyed by operation name"
765
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
691
766
  }
692
767
 
693
- output "lambda_invoke_arn" {
694
- description = "Invoke ARN of the Lambda function"
695
- value = aws_lambda_function.api_lambda.invoke_arn
768
+ output "lambda_function_arns" {
769
+ description = "ARN of each operation's Lambda function, keyed by operation name"
770
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
696
771
  }
697
772
 
698
- output "lambda_qualified_arn" {
699
- description = "Qualified ARN of the Lambda function"
700
- value = aws_lambda_function.api_lambda.qualified_arn
773
+ output "lambda_invoke_arns" {
774
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
775
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
701
776
  }
702
777
 
703
- output "lambda_version" {
704
- description = "Version of the Lambda function"
705
- value = aws_lambda_function.api_lambda.version
778
+ output "lambda_qualified_arns" {
779
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
780
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
706
781
  }
707
782
 
708
- output "lambda_source_code_hash" {
709
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
710
- value = aws_lambda_function.api_lambda.source_code_hash
783
+ output "lambda_versions" {
784
+ description = "Version of each operation's Lambda function, keyed by operation name"
785
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
711
786
  }
712
787
 
713
- output "lambda_source_code_size" {
714
- description = "Size of the Lambda deployment package in bytes"
715
- value = aws_lambda_function.api_lambda.source_code_size
788
+ output "lambda_source_code_hash" {
789
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
790
+ value = data.archive_file.lambda_zip.output_base64sha256
716
791
  }
717
792
 
718
- # IAM Role Outputs
719
- output "lambda_execution_role_arn" {
720
- description = "ARN of the Lambda execution role"
721
- value = aws_iam_role.lambda_execution_role.arn
793
+ # IAM Role Outputs, keyed by operation name
794
+ output "lambda_execution_role_arns" {
795
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
796
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
722
797
  }
723
798
 
724
- output "lambda_execution_role_name" {
725
- description = "Name of the Lambda execution role"
726
- value = aws_iam_role.lambda_execution_role.name
799
+ output "lambda_execution_role_names" {
800
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
801
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
727
802
  }
728
803
 
729
804
  output "security_group_id" {
730
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
805
+ 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."
731
806
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
732
807
  }
733
808
 
734
- # Integration Outputs
735
- output "integration_id" {
736
- description = "ID of the Lambda integration"
737
- value = aws_apigatewayv2_integration.lambda_integration.id
809
+ # Integration Outputs, keyed by operation name
810
+ output "integration_ids" {
811
+ description = "ID of each operation's Lambda integration, keyed by operation name"
812
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
738
813
  }
739
814
 
740
- # CloudWatch Log Groups
741
- output "lambda_log_group_name" {
742
- description = "Name of the Lambda CloudWatch log group"
743
- value = aws_cloudwatch_log_group.lambda_logs.name
815
+ # CloudWatch Log Groups, keyed by operation name
816
+ output "lambda_log_group_names" {
817
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
818
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
744
819
  }
745
820
 
746
- output "lambda_log_group_arn" {
747
- description = "ARN of the Lambda CloudWatch log group"
748
- value = aws_cloudwatch_log_group.lambda_logs.arn
821
+ output "lambda_log_group_arns" {
822
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
823
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
749
824
  }
750
825
 
751
826
  output "api_log_group_name" {
@@ -1016,10 +1091,18 @@ output "api_log_group_arn" {
1016
1091
  required_version = ">= 1.0"
1017
1092
 
1018
1093
  required_providers {
1094
+ archive = {
1095
+ source = "hashicorp/archive"
1096
+ version = "2.8.0"
1097
+ }
1019
1098
  aws = {
1020
1099
  source = "hashicorp/aws"
1021
1100
  version = "6.61.0"
1022
1101
  }
1102
+ random = {
1103
+ source = "hashicorp/random"
1104
+ version = "3.9.0"
1105
+ }
1023
1106
  }
1024
1107
  }
1025
1108
 
@@ -1129,6 +1212,49 @@ resource "random_string" "suffix" {
1129
1212
  upper = false
1130
1213
  }
1131
1214
 
1215
+ locals {
1216
+ # Generated at build time from the API definition
1217
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
1218
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
1219
+
1220
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
1221
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
1222
+
1223
+ function_name = { for op in keys(local.operations) : op =>
1224
+ "\${substr("TestApi-\${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}"
1225
+ }
1226
+ role_name = { for op in keys(local.operations) : op =>
1227
+ "\${substr("TestApi-\${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}"
1228
+ }
1229
+
1230
+ operation_path = { for op, details in local.operations : op =>
1231
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
1232
+ }
1233
+ route_key = { for op, details in local.operations : op =>
1234
+ "\${upper(details.method)} \${local.operation_path[op]}"
1235
+ }
1236
+
1237
+ permission_source_suffix = { for op, details in local.operations : op =>
1238
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
1239
+ }
1240
+ }
1241
+
1242
+ resource "terraform_data" "operations_metadata" {
1243
+ # Fails the plan when the operations metadata has not been generated
1244
+ input = local.operations_file
1245
+
1246
+ lifecycle {
1247
+ precondition {
1248
+ condition = fileexists(local.operations_file)
1249
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
1250
+ }
1251
+ precondition {
1252
+ condition = length(local.operations) > 0
1253
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
1254
+ }
1255
+ }
1256
+ }
1257
+
1132
1258
  # Resources
1133
1259
 
1134
1260
  # Create Lambda ZIP file from the bundle directory
@@ -1189,9 +1315,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
1189
1315
  description = "Allow outbound HTTPS to AWS service endpoints"
1190
1316
  }
1191
1317
 
1192
- # Lambda function
1193
- # This configures a single "router" lambda to serve all requests
1194
1318
  resource "aws_lambda_function" "api_lambda" {
1319
+ # One lambda function per operation, each serving just that operation
1320
+ for_each = local.operations
1321
+
1195
1322
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
1196
1323
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
1197
1324
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -1200,8 +1327,8 @@ resource "aws_lambda_function" "api_lambda" {
1200
1327
  s3_bucket = aws_s3_object.lambda_zip.bucket
1201
1328
  s3_key = aws_s3_object.lambda_zip.key
1202
1329
  s3_object_version = aws_s3_object.lambda_zip.version_id
1203
- function_name = "TestApiHandler-\${random_string.suffix.result}"
1204
- role = aws_iam_role.lambda_execution_role.arn
1330
+ function_name = local.function_name[each.key]
1331
+ role = aws_iam_role.lambda_execution_role[each.key].arn
1205
1332
  handler = "run.sh"
1206
1333
  runtime = "python3.14"
1207
1334
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -1246,7 +1373,9 @@ resource "aws_lambda_function" "api_lambda" {
1246
1373
 
1247
1374
  # IAM role for Lambda execution
1248
1375
  resource "aws_iam_role" "lambda_execution_role" {
1249
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
1376
+ for_each = local.operations
1377
+
1378
+ name = local.role_name[each.key]
1250
1379
 
1251
1380
  assume_role_policy = jsonencode({
1252
1381
  Version = "2012-10-17"
@@ -1266,32 +1395,38 @@ resource "aws_iam_role" "lambda_execution_role" {
1266
1395
 
1267
1396
  # Attach basic execution policy to Lambda role
1268
1397
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
1398
+ for_each = local.operations
1399
+
1269
1400
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
1270
- role = aws_iam_role.lambda_execution_role.name
1401
+ role = aws_iam_role.lambda_execution_role[each.key].name
1271
1402
  }
1272
1403
 
1273
1404
  # Attach X-Ray tracing policy to Lambda role
1274
1405
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
1406
+ for_each = local.operations
1407
+
1275
1408
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
1276
- role = aws_iam_role.lambda_execution_role.name
1409
+ role = aws_iam_role.lambda_execution_role[each.key].name
1277
1410
  }
1278
1411
 
1279
1412
  # Attach VPC access policy to Lambda role when deployed into a VPC
1280
1413
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
1281
- count = var.enable_vpc ? 1 : 0
1414
+ for_each = var.enable_vpc ? local.operations : {}
1415
+
1282
1416
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
1283
- role = aws_iam_role.lambda_execution_role.name
1417
+ role = aws_iam_role.lambda_execution_role[each.key].name
1284
1418
  }
1285
1419
 
1286
1420
  # Additional IAM policies for Lambda (if provided)
1287
1421
  resource "aws_iam_role_policy" "lambda_additional_policies" {
1288
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
1289
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
1290
- role = aws_iam_role.lambda_execution_role.id
1422
+ for_each = local.operations
1423
+
1424
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
1425
+ role = aws_iam_role.lambda_execution_role[each.key].id
1291
1426
 
1292
1427
  policy = jsonencode({
1293
1428
  Version = "2012-10-17"
1294
- Statement = local.lambda_policy_statements
1429
+ Statement = local.lambda_policy_document_statements
1295
1430
  })
1296
1431
  }
1297
1432
 
@@ -1307,6 +1442,15 @@ locals {
1307
1442
  Resource = ["\${var.appconfig_application_arn}/*"]
1308
1443
  }
1309
1444
  ] : [], var.additional_iam_policy_statements)
1445
+
1446
+ # IAM rejects an empty statement list, so fall back to a no-op deny
1447
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
1448
+ {
1449
+ Effect = "Deny"
1450
+ Action = ["appconfig:GetLatestConfiguration"]
1451
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
1452
+ }
1453
+ ]
1310
1454
  }
1311
1455
 
1312
1456
  # CloudWatch Log Group for Lambda
@@ -1314,15 +1458,19 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
1314
1458
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
1315
1459
  #checkov:skip=CKV_AWS_338:Log retention set to forever
1316
1460
  #checkov:skip=CKV_AWS_66:Log retention set to forever
1317
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
1318
- tags = var.tags
1461
+ for_each = local.operations
1462
+
1463
+ name = "/aws/lambda/\${local.function_name[each.key]}"
1464
+ tags = var.tags
1319
1465
  }
1320
1466
 
1321
1467
  # Lambda alias pointing to the latest published version for SnapStart
1322
1468
  resource "aws_lambda_alias" "live" {
1469
+ for_each = local.operations
1470
+
1323
1471
  name = "live"
1324
- function_name = aws_lambda_function.api_lambda.function_name
1325
- function_version = aws_lambda_function.api_lambda.version
1472
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1473
+ function_version = aws_lambda_function.api_lambda[each.key].version
1326
1474
 
1327
1475
  depends_on = [aws_lambda_function.api_lambda]
1328
1476
  }
@@ -1409,7 +1557,8 @@ resource "aws_apigatewayv2_authorizer" "custom_authorizer" {
1409
1557
  authorizer_type = "REQUEST"
1410
1558
  authorizer_uri = aws_lambda_function.authorizer_lambda.invoke_arn
1411
1559
  authorizer_payload_format_version = "2.0"
1412
- authorizer_result_ttl_in_seconds = 0
1560
+ authorizer_result_ttl_in_seconds = 300
1561
+ identity_sources = ["$request.header.Authorization"]
1413
1562
  enable_simple_responses = true
1414
1563
  name = "TestApiAuthorizer-\${random_string.suffix.result}"
1415
1564
  }
@@ -1424,9 +1573,11 @@ resource "aws_lambda_permission" "authorizer_invoke" {
1424
1573
 
1425
1574
  # Lambda integration for HTTP API
1426
1575
  resource "aws_apigatewayv2_integration" "lambda_integration" {
1576
+ for_each = local.operations
1577
+
1427
1578
  api_id = module.http_api.api_id
1428
1579
  integration_type = "AWS_PROXY"
1429
- integration_uri = aws_lambda_alias.live.invoke_arn
1580
+ integration_uri = aws_lambda_alias.live[each.key].invoke_arn
1430
1581
 
1431
1582
  payload_format_version = "2.0"
1432
1583
  timeout_milliseconds = 30000
@@ -1434,15 +1585,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
1434
1585
  depends_on = [aws_lambda_alias.live]
1435
1586
  }
1436
1587
 
1437
- # Route for proxy integration (catches all requests)
1438
- resource "aws_apigatewayv2_route" "proxy_routes" {
1439
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
1440
- # when cors settings are configured
1441
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
1588
+ # One route per operation, each targeting that operation's integration
1589
+ resource "aws_apigatewayv2_route" "operation_routes" {
1590
+ for_each = local.operations
1442
1591
 
1443
1592
  api_id = module.http_api.api_id
1444
- route_key = "\${each.key} /{proxy+}"
1445
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
1593
+ route_key = local.route_key[each.key]
1594
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
1446
1595
 
1447
1596
  authorization_type = "CUSTOM"
1448
1597
  authorizer_id = aws_apigatewayv2_authorizer.custom_authorizer.id
@@ -1463,12 +1612,14 @@ module "add_url_to_runtime_config" {
1463
1612
 
1464
1613
  # Lambda permission for API Gateway to invoke the function via alias
1465
1614
  resource "aws_lambda_permission" "api_gateway_invoke" {
1615
+ for_each = local.operations
1616
+
1466
1617
  statement_id = "AllowExecutionFromAPIGateway"
1467
1618
  action = "lambda:InvokeFunction"
1468
- function_name = aws_lambda_function.api_lambda.function_name
1469
- qualifier = aws_lambda_alias.live.name
1619
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1620
+ qualifier = aws_lambda_alias.live[each.key].name
1470
1621
  principal = "apigateway.amazonaws.com"
1471
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
1622
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
1472
1623
 
1473
1624
  depends_on = [module.http_api, aws_lambda_alias.live]
1474
1625
  }
@@ -1511,73 +1662,73 @@ output "stage_execution_arn" {
1511
1662
  value = module.http_api.stage_execution_arn
1512
1663
  }
1513
1664
 
1514
- # Lambda Function Outputs
1515
- output "lambda_function_name" {
1516
- description = "Name of the Lambda function"
1517
- value = aws_lambda_function.api_lambda.function_name
1665
+ # Lambda Function Outputs, keyed by operation name
1666
+ output "operations" {
1667
+ description = "Names of the API operations, each with its own Lambda function"
1668
+ value = keys(local.operations)
1518
1669
  }
1519
1670
 
1520
- output "lambda_function_arn" {
1521
- description = "ARN of the Lambda function"
1522
- value = aws_lambda_function.api_lambda.arn
1671
+ output "lambda_function_names" {
1672
+ description = "Name of each operation's Lambda function, keyed by operation name"
1673
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
1523
1674
  }
1524
1675
 
1525
- output "lambda_invoke_arn" {
1526
- description = "Invoke ARN of the Lambda function"
1527
- value = aws_lambda_function.api_lambda.invoke_arn
1676
+ output "lambda_function_arns" {
1677
+ description = "ARN of each operation's Lambda function, keyed by operation name"
1678
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
1528
1679
  }
1529
1680
 
1530
- output "lambda_qualified_arn" {
1531
- description = "Qualified ARN of the Lambda function"
1532
- value = aws_lambda_function.api_lambda.qualified_arn
1681
+ output "lambda_invoke_arns" {
1682
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
1683
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
1533
1684
  }
1534
1685
 
1535
- output "lambda_version" {
1536
- description = "Version of the Lambda function"
1537
- value = aws_lambda_function.api_lambda.version
1686
+ output "lambda_qualified_arns" {
1687
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
1688
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
1538
1689
  }
1539
1690
 
1540
- output "lambda_source_code_hash" {
1541
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
1542
- value = aws_lambda_function.api_lambda.source_code_hash
1691
+ output "lambda_versions" {
1692
+ description = "Version of each operation's Lambda function, keyed by operation name"
1693
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
1543
1694
  }
1544
1695
 
1545
- output "lambda_source_code_size" {
1546
- description = "Size of the Lambda deployment package in bytes"
1547
- value = aws_lambda_function.api_lambda.source_code_size
1696
+ output "lambda_source_code_hash" {
1697
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
1698
+ value = data.archive_file.lambda_zip.output_base64sha256
1548
1699
  }
1549
1700
 
1550
- # IAM Role Outputs
1551
- output "lambda_execution_role_arn" {
1552
- description = "ARN of the Lambda execution role"
1553
- value = aws_iam_role.lambda_execution_role.arn
1701
+ # IAM Role Outputs, keyed by operation name
1702
+ output "lambda_execution_role_arns" {
1703
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
1704
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
1554
1705
  }
1555
1706
 
1556
- output "lambda_execution_role_name" {
1557
- description = "Name of the Lambda execution role"
1558
- value = aws_iam_role.lambda_execution_role.name
1707
+ output "lambda_execution_role_names" {
1708
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
1709
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
1559
1710
  }
1560
1711
 
1561
1712
  output "security_group_id" {
1562
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
1713
+ 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."
1563
1714
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
1564
1715
  }
1565
1716
 
1566
- # Integration Outputs
1567
- output "integration_id" {
1568
- description = "ID of the Lambda integration"
1569
- value = aws_apigatewayv2_integration.lambda_integration.id
1717
+ # Integration Outputs, keyed by operation name
1718
+ output "integration_ids" {
1719
+ description = "ID of each operation's Lambda integration, keyed by operation name"
1720
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
1570
1721
  }
1571
1722
 
1572
- # CloudWatch Log Groups
1573
- output "lambda_log_group_name" {
1574
- description = "Name of the Lambda CloudWatch log group"
1575
- value = aws_cloudwatch_log_group.lambda_logs.name
1723
+ # CloudWatch Log Groups, keyed by operation name
1724
+ output "lambda_log_group_names" {
1725
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
1726
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
1576
1727
  }
1577
1728
 
1578
- output "lambda_log_group_arn" {
1579
- description = "ARN of the Lambda CloudWatch log group"
1580
- value = aws_cloudwatch_log_group.lambda_logs.arn
1729
+ output "lambda_log_group_arns" {
1730
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
1731
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
1581
1732
  }
1582
1733
 
1583
1734
  output "api_log_group_name" {
@@ -1848,10 +1999,18 @@ output "api_log_group_arn" {
1848
1999
  required_version = ">= 1.0"
1849
2000
 
1850
2001
  required_providers {
2002
+ archive = {
2003
+ source = "hashicorp/archive"
2004
+ version = "2.8.0"
2005
+ }
1851
2006
  aws = {
1852
2007
  source = "hashicorp/aws"
1853
2008
  version = "6.61.0"
1854
2009
  }
2010
+ random = {
2011
+ source = "hashicorp/random"
2012
+ version = "3.9.0"
2013
+ }
1855
2014
  }
1856
2015
  }
1857
2016
 
@@ -1961,6 +2120,49 @@ resource "random_string" "suffix" {
1961
2120
  upper = false
1962
2121
  }
1963
2122
 
2123
+ locals {
2124
+ # Generated at build time from the API definition
2125
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
2126
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
2127
+
2128
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
2129
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
2130
+
2131
+ function_name = { for op in keys(local.operations) : op =>
2132
+ "\${substr("TestApi-\${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}"
2133
+ }
2134
+ role_name = { for op in keys(local.operations) : op =>
2135
+ "\${substr("TestApi-\${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}"
2136
+ }
2137
+
2138
+ operation_path = { for op, details in local.operations : op =>
2139
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
2140
+ }
2141
+ route_key = { for op, details in local.operations : op =>
2142
+ "\${upper(details.method)} \${local.operation_path[op]}"
2143
+ }
2144
+
2145
+ permission_source_suffix = { for op, details in local.operations : op =>
2146
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
2147
+ }
2148
+ }
2149
+
2150
+ resource "terraform_data" "operations_metadata" {
2151
+ # Fails the plan when the operations metadata has not been generated
2152
+ input = local.operations_file
2153
+
2154
+ lifecycle {
2155
+ precondition {
2156
+ condition = fileexists(local.operations_file)
2157
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
2158
+ }
2159
+ precondition {
2160
+ condition = length(local.operations) > 0
2161
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
2162
+ }
2163
+ }
2164
+ }
2165
+
1964
2166
  # Resources
1965
2167
 
1966
2168
  # Create Lambda ZIP file from the bundle directory
@@ -2021,9 +2223,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
2021
2223
  description = "Allow outbound HTTPS to AWS service endpoints"
2022
2224
  }
2023
2225
 
2024
- # Lambda function
2025
- # This configures a single "router" lambda to serve all requests
2026
2226
  resource "aws_lambda_function" "api_lambda" {
2227
+ # One lambda function per operation, each serving just that operation
2228
+ for_each = local.operations
2229
+
2027
2230
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
2028
2231
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
2029
2232
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -2032,8 +2235,8 @@ resource "aws_lambda_function" "api_lambda" {
2032
2235
  s3_bucket = aws_s3_object.lambda_zip.bucket
2033
2236
  s3_key = aws_s3_object.lambda_zip.key
2034
2237
  s3_object_version = aws_s3_object.lambda_zip.version_id
2035
- function_name = "TestApiHandler-\${random_string.suffix.result}"
2036
- role = aws_iam_role.lambda_execution_role.arn
2238
+ function_name = local.function_name[each.key]
2239
+ role = aws_iam_role.lambda_execution_role[each.key].arn
2037
2240
  handler = "run.sh"
2038
2241
  runtime = "python3.14"
2039
2242
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -2078,7 +2281,9 @@ resource "aws_lambda_function" "api_lambda" {
2078
2281
 
2079
2282
  # IAM role for Lambda execution
2080
2283
  resource "aws_iam_role" "lambda_execution_role" {
2081
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
2284
+ for_each = local.operations
2285
+
2286
+ name = local.role_name[each.key]
2082
2287
 
2083
2288
  assume_role_policy = jsonencode({
2084
2289
  Version = "2012-10-17"
@@ -2098,32 +2303,38 @@ resource "aws_iam_role" "lambda_execution_role" {
2098
2303
 
2099
2304
  # Attach basic execution policy to Lambda role
2100
2305
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
2306
+ for_each = local.operations
2307
+
2101
2308
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
2102
- role = aws_iam_role.lambda_execution_role.name
2309
+ role = aws_iam_role.lambda_execution_role[each.key].name
2103
2310
  }
2104
2311
 
2105
2312
  # Attach X-Ray tracing policy to Lambda role
2106
2313
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
2314
+ for_each = local.operations
2315
+
2107
2316
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
2108
- role = aws_iam_role.lambda_execution_role.name
2317
+ role = aws_iam_role.lambda_execution_role[each.key].name
2109
2318
  }
2110
2319
 
2111
2320
  # Attach VPC access policy to Lambda role when deployed into a VPC
2112
2321
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
2113
- count = var.enable_vpc ? 1 : 0
2322
+ for_each = var.enable_vpc ? local.operations : {}
2323
+
2114
2324
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
2115
- role = aws_iam_role.lambda_execution_role.name
2325
+ role = aws_iam_role.lambda_execution_role[each.key].name
2116
2326
  }
2117
2327
 
2118
2328
  # Additional IAM policies for Lambda (if provided)
2119
2329
  resource "aws_iam_role_policy" "lambda_additional_policies" {
2120
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
2121
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
2122
- role = aws_iam_role.lambda_execution_role.id
2330
+ for_each = local.operations
2331
+
2332
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
2333
+ role = aws_iam_role.lambda_execution_role[each.key].id
2123
2334
 
2124
2335
  policy = jsonencode({
2125
2336
  Version = "2012-10-17"
2126
- Statement = local.lambda_policy_statements
2337
+ Statement = local.lambda_policy_document_statements
2127
2338
  })
2128
2339
  }
2129
2340
 
@@ -2139,6 +2350,15 @@ locals {
2139
2350
  Resource = ["\${var.appconfig_application_arn}/*"]
2140
2351
  }
2141
2352
  ] : [], var.additional_iam_policy_statements)
2353
+
2354
+ # IAM rejects an empty statement list, so fall back to a no-op deny
2355
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
2356
+ {
2357
+ Effect = "Deny"
2358
+ Action = ["appconfig:GetLatestConfiguration"]
2359
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
2360
+ }
2361
+ ]
2142
2362
  }
2143
2363
 
2144
2364
  # CloudWatch Log Group for Lambda
@@ -2146,15 +2366,19 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
2146
2366
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2147
2367
  #checkov:skip=CKV_AWS_338:Log retention set to forever
2148
2368
  #checkov:skip=CKV_AWS_66:Log retention set to forever
2149
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
2150
- tags = var.tags
2369
+ for_each = local.operations
2370
+
2371
+ name = "/aws/lambda/\${local.function_name[each.key]}"
2372
+ tags = var.tags
2151
2373
  }
2152
2374
 
2153
2375
  # Lambda alias pointing to the latest published version for SnapStart
2154
2376
  resource "aws_lambda_alias" "live" {
2377
+ for_each = local.operations
2378
+
2155
2379
  name = "live"
2156
- function_name = aws_lambda_function.api_lambda.function_name
2157
- function_version = aws_lambda_function.api_lambda.version
2380
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
2381
+ function_version = aws_lambda_function.api_lambda[each.key].version
2158
2382
 
2159
2383
  depends_on = [aws_lambda_function.api_lambda]
2160
2384
  }
@@ -2162,9 +2386,11 @@ resource "aws_lambda_alias" "live" {
2162
2386
 
2163
2387
  # Lambda integration for HTTP API
2164
2388
  resource "aws_apigatewayv2_integration" "lambda_integration" {
2389
+ for_each = local.operations
2390
+
2165
2391
  api_id = module.http_api.api_id
2166
2392
  integration_type = "AWS_PROXY"
2167
- integration_uri = aws_lambda_alias.live.invoke_arn
2393
+ integration_uri = aws_lambda_alias.live[each.key].invoke_arn
2168
2394
 
2169
2395
  payload_format_version = "2.0"
2170
2396
  timeout_milliseconds = 30000
@@ -2172,15 +2398,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
2172
2398
  depends_on = [aws_lambda_alias.live]
2173
2399
  }
2174
2400
 
2175
- # Route for proxy integration (catches all requests)
2176
- resource "aws_apigatewayv2_route" "proxy_routes" {
2177
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
2178
- # when cors settings are configured
2179
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
2401
+ # One route per operation, each targeting that operation's integration
2402
+ resource "aws_apigatewayv2_route" "operation_routes" {
2403
+ for_each = local.operations
2180
2404
 
2181
2405
  api_id = module.http_api.api_id
2182
- route_key = "\${each.key} /{proxy+}"
2183
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
2406
+ route_key = local.route_key[each.key]
2407
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
2184
2408
 
2185
2409
  authorization_type = "AWS_IAM"
2186
2410
 
@@ -2200,12 +2424,14 @@ module "add_url_to_runtime_config" {
2200
2424
 
2201
2425
  # Lambda permission for API Gateway to invoke the function via alias
2202
2426
  resource "aws_lambda_permission" "api_gateway_invoke" {
2427
+ for_each = local.operations
2428
+
2203
2429
  statement_id = "AllowExecutionFromAPIGateway"
2204
2430
  action = "lambda:InvokeFunction"
2205
- function_name = aws_lambda_function.api_lambda.function_name
2206
- qualifier = aws_lambda_alias.live.name
2431
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
2432
+ qualifier = aws_lambda_alias.live[each.key].name
2207
2433
  principal = "apigateway.amazonaws.com"
2208
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
2434
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
2209
2435
 
2210
2436
  depends_on = [module.http_api, aws_lambda_alias.live]
2211
2437
  }
@@ -2248,73 +2474,73 @@ output "stage_execution_arn" {
2248
2474
  value = module.http_api.stage_execution_arn
2249
2475
  }
2250
2476
 
2251
- # Lambda Function Outputs
2252
- output "lambda_function_name" {
2253
- description = "Name of the Lambda function"
2254
- value = aws_lambda_function.api_lambda.function_name
2477
+ # Lambda Function Outputs, keyed by operation name
2478
+ output "operations" {
2479
+ description = "Names of the API operations, each with its own Lambda function"
2480
+ value = keys(local.operations)
2255
2481
  }
2256
2482
 
2257
- output "lambda_function_arn" {
2258
- description = "ARN of the Lambda function"
2259
- value = aws_lambda_function.api_lambda.arn
2483
+ output "lambda_function_names" {
2484
+ description = "Name of each operation's Lambda function, keyed by operation name"
2485
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
2260
2486
  }
2261
2487
 
2262
- output "lambda_invoke_arn" {
2263
- description = "Invoke ARN of the Lambda function"
2264
- value = aws_lambda_function.api_lambda.invoke_arn
2488
+ output "lambda_function_arns" {
2489
+ description = "ARN of each operation's Lambda function, keyed by operation name"
2490
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
2265
2491
  }
2266
2492
 
2267
- output "lambda_qualified_arn" {
2268
- description = "Qualified ARN of the Lambda function"
2269
- value = aws_lambda_function.api_lambda.qualified_arn
2493
+ output "lambda_invoke_arns" {
2494
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
2495
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
2270
2496
  }
2271
2497
 
2272
- output "lambda_version" {
2273
- description = "Version of the Lambda function"
2274
- value = aws_lambda_function.api_lambda.version
2498
+ output "lambda_qualified_arns" {
2499
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
2500
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
2275
2501
  }
2276
2502
 
2277
- output "lambda_source_code_hash" {
2278
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
2279
- value = aws_lambda_function.api_lambda.source_code_hash
2503
+ output "lambda_versions" {
2504
+ description = "Version of each operation's Lambda function, keyed by operation name"
2505
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
2280
2506
  }
2281
2507
 
2282
- output "lambda_source_code_size" {
2283
- description = "Size of the Lambda deployment package in bytes"
2284
- value = aws_lambda_function.api_lambda.source_code_size
2508
+ output "lambda_source_code_hash" {
2509
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
2510
+ value = data.archive_file.lambda_zip.output_base64sha256
2285
2511
  }
2286
2512
 
2287
- # IAM Role Outputs
2288
- output "lambda_execution_role_arn" {
2289
- description = "ARN of the Lambda execution role"
2290
- value = aws_iam_role.lambda_execution_role.arn
2513
+ # IAM Role Outputs, keyed by operation name
2514
+ output "lambda_execution_role_arns" {
2515
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
2516
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
2291
2517
  }
2292
2518
 
2293
- output "lambda_execution_role_name" {
2294
- description = "Name of the Lambda execution role"
2295
- value = aws_iam_role.lambda_execution_role.name
2519
+ output "lambda_execution_role_names" {
2520
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
2521
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
2296
2522
  }
2297
2523
 
2298
2524
  output "security_group_id" {
2299
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
2525
+ 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."
2300
2526
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
2301
2527
  }
2302
2528
 
2303
- # Integration Outputs
2304
- output "integration_id" {
2305
- description = "ID of the Lambda integration"
2306
- value = aws_apigatewayv2_integration.lambda_integration.id
2529
+ # Integration Outputs, keyed by operation name
2530
+ output "integration_ids" {
2531
+ description = "ID of each operation's Lambda integration, keyed by operation name"
2532
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
2307
2533
  }
2308
2534
 
2309
- # CloudWatch Log Groups
2310
- output "lambda_log_group_name" {
2311
- description = "Name of the Lambda CloudWatch log group"
2312
- value = aws_cloudwatch_log_group.lambda_logs.name
2535
+ # CloudWatch Log Groups, keyed by operation name
2536
+ output "lambda_log_group_names" {
2537
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
2538
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
2313
2539
  }
2314
2540
 
2315
- output "lambda_log_group_arn" {
2316
- description = "ARN of the Lambda CloudWatch log group"
2317
- value = aws_cloudwatch_log_group.lambda_logs.arn
2541
+ output "lambda_log_group_arns" {
2542
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
2543
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
2318
2544
  }
2319
2545
 
2320
2546
  output "api_log_group_name" {
@@ -2533,22 +2759,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
2533
2759
  }
2534
2760
  }
2535
2761
 
2762
+ resource "random_id" "waf_log_suffix" {
2763
+ count = var.enable_waf ? 1 : 0
2764
+ byte_length = 4
2765
+ }
2766
+
2767
+ locals {
2768
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
2769
+ }
2770
+
2771
+ # KMS key for encrypting WAF request logs at rest
2772
+ resource "aws_kms_key" "waf_logs" {
2773
+ count = var.enable_waf ? 1 : 0
2774
+
2775
+ description = "\${var.api_name} API WAF log encryption"
2776
+ deletion_window_in_days = 7
2777
+ enable_key_rotation = true
2778
+
2779
+ policy = jsonencode({
2780
+ Version = "2012-10-17"
2781
+ Statement = [
2782
+ {
2783
+ Sid = "Enable IAM User Permissions"
2784
+ Effect = "Allow"
2785
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
2786
+ Action = "kms:*"
2787
+ Resource = "*"
2788
+ },
2789
+ {
2790
+ Sid = "Allow CloudWatch Logs"
2791
+ Effect = "Allow"
2792
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
2793
+ Action = [
2794
+ "kms:Encrypt",
2795
+ "kms:Decrypt",
2796
+ "kms:ReEncrypt*",
2797
+ "kms:GenerateDataKey*",
2798
+ "kms:DescribeKey"
2799
+ ]
2800
+ Resource = "*"
2801
+ Condition = {
2802
+ ArnEquals = {
2803
+ "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}"
2804
+ }
2805
+ }
2806
+ }
2807
+ ]
2808
+ })
2809
+
2810
+ tags = var.tags
2811
+ }
2812
+
2813
+ resource "aws_kms_alias" "waf_logs" {
2814
+ count = var.enable_waf ? 1 : 0
2815
+
2816
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
2817
+ target_key_id = aws_kms_key.waf_logs[0].key_id
2818
+ }
2819
+
2536
2820
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
2537
2821
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
2538
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2539
2822
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
2540
2823
  count = var.enable_waf ? 1 : 0
2541
2824
 
2542
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
2825
+ name = local.waf_log_group_name
2543
2826
  retention_in_days = 30
2827
+ kms_key_id = aws_kms_key.waf_logs[0].arn
2544
2828
  tags = var.tags
2545
2829
  }
2546
2830
 
2547
- resource "random_id" "waf_log_suffix" {
2548
- count = var.enable_waf ? 1 : 0
2549
- byte_length = 4
2550
- }
2551
-
2552
2831
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
2553
2832
  count = var.enable_waf ? 1 : 0
2554
2833
 
@@ -2594,10 +2873,18 @@ output "waf_web_acl_arn" {
2594
2873
  required_version = ">= 1.0"
2595
2874
 
2596
2875
  required_providers {
2876
+ archive = {
2877
+ source = "hashicorp/archive"
2878
+ version = "2.8.0"
2879
+ }
2597
2880
  aws = {
2598
2881
  source = "hashicorp/aws"
2599
2882
  version = "6.61.0"
2600
2883
  }
2884
+ random = {
2885
+ source = "hashicorp/random"
2886
+ version = "3.9.0"
2887
+ }
2601
2888
  }
2602
2889
  }
2603
2890
 
@@ -2713,63 +3000,292 @@ resource "random_string" "suffix" {
2713
3000
  upper = false
2714
3001
  }
2715
3002
 
2716
- # Resources
3003
+ locals {
3004
+ # Generated at build time from the API definition
3005
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
3006
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
2717
3007
 
2718
- # Create Lambda ZIP file from the bundle directory
2719
- data "archive_file" "lambda_zip" {
2720
- type = "zip"
2721
- source_dir = "\${path.module}/../../../../../../../dist/apps/test_api/bundle-x86"
2722
- output_path = "\${path.module}/../../../../../../../dist/packages/common/terraform/apis/test-api/lambda.zip"
3008
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
3009
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
3010
+
3011
+ function_name = { for op in keys(local.operations) : op =>
3012
+ "\${substr("TestApi-\${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}"
3013
+ }
3014
+ role_name = { for op in keys(local.operations) : op =>
3015
+ "\${substr("TestApi-\${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}"
3016
+ }
3017
+
3018
+ # A REST API needs a resource per path segment, so each operation's path is
3019
+ # split into the distinct path prefixes the resource tree is built from below
3020
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
3021
+
3022
+ path_nodes = merge([
3023
+ for op, segments in local.operation_segments : {
3024
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
3025
+ depth = i
3026
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
3027
+ part = segment
3028
+ }
3029
+ }
3030
+ ]...)
3031
+
3032
+ # Instances of a resource cannot reference one another, so each depth is a
3033
+ # separate resource. Deeper paths are reported by the precondition below.
3034
+ max_path_depth = 16
3035
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
3036
+ for path, node in local.path_nodes : path => node if node.depth == depth
3037
+ } }
3038
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
3039
+
3040
+ resource_ids = merge(
3041
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
3042
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
3043
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
3044
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
3045
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
3046
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
3047
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
3048
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
3049
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
3050
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
3051
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
3052
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
3053
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
3054
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
3055
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
3056
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
3057
+ )
3058
+
3059
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
3060
+ local.resource_ids[join("/", segments)]
3061
+ }
3062
+
3063
+ permission_source_suffix = { for op, details in local.operations : op =>
3064
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
3065
+ }
2723
3066
  }
2724
3067
 
2725
- resource "aws_s3_object" "lambda_zip" {
2726
- bucket = var.asset_bucket_name
2727
- key = "apis/test-api/\${data.archive_file.lambda_zip.output_sha256}.zip"
2728
- source = data.archive_file.lambda_zip.output_path
2729
- source_hash = data.archive_file.lambda_zip.output_base64sha256
2730
- etag = data.archive_file.lambda_zip.output_md5
3068
+ resource "terraform_data" "operations_metadata" {
3069
+ # Fails the plan when the operations metadata has not been generated
3070
+ input = local.operations_file
3071
+
3072
+ lifecycle {
3073
+ precondition {
3074
+ condition = fileexists(local.operations_file)
3075
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
3076
+ }
3077
+ precondition {
3078
+ condition = length(local.operations) > 0
3079
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
3080
+ }
3081
+ precondition {
3082
+ condition = length(local.paths_too_deep) == 0
3083
+ 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."
3084
+ }
3085
+ }
2731
3086
  }
2732
3087
 
2733
- # Use the core REST API module
2734
- # Account-level CloudWatch role required for REST API access logging
2735
- module "account" {
2736
- source = "../../../core/api/api-gateway-account"
3088
+ # API Gateway resources at path depth 0
3089
+ resource "aws_api_gateway_resource" "path_depth_0" {
3090
+ for_each = local.path_nodes_by_depth[0]
3091
+
3092
+ rest_api_id = module.rest_api.api_id
3093
+ parent_id = module.rest_api.api_root_resource_id
3094
+ path_part = each.value.part
2737
3095
  }
2738
3096
 
2739
- module "rest_api" {
2740
- source = "../../../core/api/rest-api"
3097
+ # API Gateway resources at path depth 1
3098
+ resource "aws_api_gateway_resource" "path_depth_1" {
3099
+ for_each = local.path_nodes_by_depth[1]
2741
3100
 
2742
- api_name = "TestApi-\${random_string.suffix.result}"
2743
- api_description = "TestApi REST API"
2744
- stage_name = "prod"
2745
- stage_auto_deploy = true
3101
+ rest_api_id = module.rest_api.api_id
3102
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
3103
+ path_part = each.value.part
3104
+ }
2746
3105
 
2747
- # WAF Configuration
2748
- enable_waf = var.enable_waf
3106
+ # API Gateway resources at path depth 2
3107
+ resource "aws_api_gateway_resource" "path_depth_2" {
3108
+ for_each = local.path_nodes_by_depth[2]
2749
3109
 
2750
- # CORS Configuration
2751
- cors_allow_headers = var.cors_allow_headers
2752
- cors_allow_methods = var.cors_allow_methods
2753
- cors_allow_origins = var.cors_allow_origins
3110
+ rest_api_id = module.rest_api.api_id
3111
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
3112
+ path_part = each.value.part
3113
+ }
2754
3114
 
2755
- # Tags
2756
- tags = var.tags
3115
+ # API Gateway resources at path depth 3
3116
+ resource "aws_api_gateway_resource" "path_depth_3" {
3117
+ for_each = local.path_nodes_by_depth[3]
3118
+
3119
+ rest_api_id = module.rest_api.api_id
3120
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
3121
+ path_part = each.value.part
2757
3122
  }
2758
3123
 
2759
- resource "aws_wafv2_web_acl_association" "api_waf_association" {
2760
- count = var.enable_waf ? 1 : 0
3124
+ # API Gateway resources at path depth 4
3125
+ resource "aws_api_gateway_resource" "path_depth_4" {
3126
+ for_each = local.path_nodes_by_depth[4]
2761
3127
 
2762
- resource_arn = aws_api_gateway_stage.api_stage.arn
2763
- web_acl_arn = module.rest_api.waf_web_acl_arn
3128
+ rest_api_id = module.rest_api.api_id
3129
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
3130
+ path_part = each.value.part
3131
+ }
2764
3132
 
2765
- depends_on = [aws_api_gateway_stage.api_stage]
3133
+ # API Gateway resources at path depth 5
3134
+ resource "aws_api_gateway_resource" "path_depth_5" {
3135
+ for_each = local.path_nodes_by_depth[5]
3136
+
3137
+ rest_api_id = module.rest_api.api_id
3138
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
3139
+ path_part = each.value.part
2766
3140
  }
2767
3141
 
2768
- # Security group for the API Lambda function, used when deployed into a VPC
2769
- resource "aws_security_group" "api_lambda" {
2770
- count = var.enable_vpc ? 1 : 0
3142
+ # API Gateway resources at path depth 6
3143
+ resource "aws_api_gateway_resource" "path_depth_6" {
3144
+ for_each = local.path_nodes_by_depth[6]
2771
3145
 
2772
- #checkov:skip=CKV2_AWS_5:Attached to api_lambda via vpc_config block; Checkov cannot resolve this reference
3146
+ rest_api_id = module.rest_api.api_id
3147
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
3148
+ path_part = each.value.part
3149
+ }
3150
+
3151
+ # API Gateway resources at path depth 7
3152
+ resource "aws_api_gateway_resource" "path_depth_7" {
3153
+ for_each = local.path_nodes_by_depth[7]
3154
+
3155
+ rest_api_id = module.rest_api.api_id
3156
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
3157
+ path_part = each.value.part
3158
+ }
3159
+
3160
+ # API Gateway resources at path depth 8
3161
+ resource "aws_api_gateway_resource" "path_depth_8" {
3162
+ for_each = local.path_nodes_by_depth[8]
3163
+
3164
+ rest_api_id = module.rest_api.api_id
3165
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
3166
+ path_part = each.value.part
3167
+ }
3168
+
3169
+ # API Gateway resources at path depth 9
3170
+ resource "aws_api_gateway_resource" "path_depth_9" {
3171
+ for_each = local.path_nodes_by_depth[9]
3172
+
3173
+ rest_api_id = module.rest_api.api_id
3174
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
3175
+ path_part = each.value.part
3176
+ }
3177
+
3178
+ # API Gateway resources at path depth 10
3179
+ resource "aws_api_gateway_resource" "path_depth_10" {
3180
+ for_each = local.path_nodes_by_depth[10]
3181
+
3182
+ rest_api_id = module.rest_api.api_id
3183
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
3184
+ path_part = each.value.part
3185
+ }
3186
+
3187
+ # API Gateway resources at path depth 11
3188
+ resource "aws_api_gateway_resource" "path_depth_11" {
3189
+ for_each = local.path_nodes_by_depth[11]
3190
+
3191
+ rest_api_id = module.rest_api.api_id
3192
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
3193
+ path_part = each.value.part
3194
+ }
3195
+
3196
+ # API Gateway resources at path depth 12
3197
+ resource "aws_api_gateway_resource" "path_depth_12" {
3198
+ for_each = local.path_nodes_by_depth[12]
3199
+
3200
+ rest_api_id = module.rest_api.api_id
3201
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
3202
+ path_part = each.value.part
3203
+ }
3204
+
3205
+ # API Gateway resources at path depth 13
3206
+ resource "aws_api_gateway_resource" "path_depth_13" {
3207
+ for_each = local.path_nodes_by_depth[13]
3208
+
3209
+ rest_api_id = module.rest_api.api_id
3210
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
3211
+ path_part = each.value.part
3212
+ }
3213
+
3214
+ # API Gateway resources at path depth 14
3215
+ resource "aws_api_gateway_resource" "path_depth_14" {
3216
+ for_each = local.path_nodes_by_depth[14]
3217
+
3218
+ rest_api_id = module.rest_api.api_id
3219
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
3220
+ path_part = each.value.part
3221
+ }
3222
+
3223
+ # API Gateway resources at path depth 15
3224
+ resource "aws_api_gateway_resource" "path_depth_15" {
3225
+ for_each = local.path_nodes_by_depth[15]
3226
+
3227
+ rest_api_id = module.rest_api.api_id
3228
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
3229
+ path_part = each.value.part
3230
+ }
3231
+
3232
+ # Resources
3233
+
3234
+ # Create Lambda ZIP file from the bundle directory
3235
+ data "archive_file" "lambda_zip" {
3236
+ type = "zip"
3237
+ source_dir = "\${path.module}/../../../../../../../dist/apps/test_api/bundle-x86"
3238
+ output_path = "\${path.module}/../../../../../../../dist/packages/common/terraform/apis/test-api/lambda.zip"
3239
+ }
3240
+
3241
+ resource "aws_s3_object" "lambda_zip" {
3242
+ bucket = var.asset_bucket_name
3243
+ key = "apis/test-api/\${data.archive_file.lambda_zip.output_sha256}.zip"
3244
+ source = data.archive_file.lambda_zip.output_path
3245
+ source_hash = data.archive_file.lambda_zip.output_base64sha256
3246
+ etag = data.archive_file.lambda_zip.output_md5
3247
+ }
3248
+
3249
+ # Use the core REST API module
3250
+ # Account-level CloudWatch role required for REST API access logging
3251
+ module "account" {
3252
+ source = "../../../core/api/api-gateway-account"
3253
+ }
3254
+
3255
+ module "rest_api" {
3256
+ source = "../../../core/api/rest-api"
3257
+
3258
+ api_name = "TestApi-\${random_string.suffix.result}"
3259
+ api_description = "TestApi REST API"
3260
+ stage_name = "prod"
3261
+ stage_auto_deploy = true
3262
+
3263
+ # WAF Configuration
3264
+ enable_waf = var.enable_waf
3265
+
3266
+ # CORS Configuration
3267
+ cors_allow_headers = var.cors_allow_headers
3268
+ cors_allow_methods = var.cors_allow_methods
3269
+ cors_allow_origins = var.cors_allow_origins
3270
+
3271
+ # Tags
3272
+ tags = var.tags
3273
+ }
3274
+
3275
+ resource "aws_wafv2_web_acl_association" "api_waf_association" {
3276
+ count = var.enable_waf ? 1 : 0
3277
+
3278
+ resource_arn = aws_api_gateway_stage.api_stage.arn
3279
+ web_acl_arn = module.rest_api.waf_web_acl_arn
3280
+
3281
+ depends_on = [aws_api_gateway_stage.api_stage]
3282
+ }
3283
+
3284
+ # Security group for the API Lambda function, used when deployed into a VPC
3285
+ resource "aws_security_group" "api_lambda" {
3286
+ count = var.enable_vpc ? 1 : 0
3287
+
3288
+ #checkov:skip=CKV2_AWS_5:Attached to api_lambda via vpc_config block; Checkov cannot resolve this reference
2773
3289
  name_prefix = "test-api-lambda-"
2774
3290
  description = "Security group for the TestApi API Lambda function"
2775
3291
  vpc_id = var.vpc_id
@@ -2787,8 +3303,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
2787
3303
  description = "Allow outbound HTTPS to AWS service endpoints"
2788
3304
  }
2789
3305
 
2790
- # Lambda function
2791
3306
  resource "aws_lambda_function" "api_lambda" {
3307
+ # One lambda function per operation, each serving just that operation
3308
+ for_each = local.operations
3309
+
2792
3310
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
2793
3311
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
2794
3312
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -2797,8 +3315,8 @@ resource "aws_lambda_function" "api_lambda" {
2797
3315
  s3_bucket = aws_s3_object.lambda_zip.bucket
2798
3316
  s3_key = aws_s3_object.lambda_zip.key
2799
3317
  s3_object_version = aws_s3_object.lambda_zip.version_id
2800
- function_name = "TestApiHandler-\${random_string.suffix.result}"
2801
- role = aws_iam_role.lambda_execution_role.arn
3318
+ function_name = local.function_name[each.key]
3319
+ role = aws_iam_role.lambda_execution_role[each.key].arn
2802
3320
  handler = "run.sh"
2803
3321
  runtime = "python3.14"
2804
3322
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -2843,7 +3361,9 @@ resource "aws_lambda_function" "api_lambda" {
2843
3361
 
2844
3362
  # IAM role for Lambda execution
2845
3363
  resource "aws_iam_role" "lambda_execution_role" {
2846
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
3364
+ for_each = local.operations
3365
+
3366
+ name = local.role_name[each.key]
2847
3367
 
2848
3368
  assume_role_policy = jsonencode({
2849
3369
  Version = "2012-10-17"
@@ -2863,32 +3383,38 @@ resource "aws_iam_role" "lambda_execution_role" {
2863
3383
 
2864
3384
  # Attach basic execution policy to Lambda role
2865
3385
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
3386
+ for_each = local.operations
3387
+
2866
3388
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
2867
- role = aws_iam_role.lambda_execution_role.name
3389
+ role = aws_iam_role.lambda_execution_role[each.key].name
2868
3390
  }
2869
3391
 
2870
3392
  # Attach X-Ray tracing policy to Lambda role
2871
3393
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
3394
+ for_each = local.operations
3395
+
2872
3396
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
2873
- role = aws_iam_role.lambda_execution_role.name
3397
+ role = aws_iam_role.lambda_execution_role[each.key].name
2874
3398
  }
2875
3399
 
2876
3400
  # Attach VPC access policy to Lambda role when deployed into a VPC
2877
3401
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
2878
- count = var.enable_vpc ? 1 : 0
3402
+ for_each = var.enable_vpc ? local.operations : {}
3403
+
2879
3404
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
2880
- role = aws_iam_role.lambda_execution_role.name
3405
+ role = aws_iam_role.lambda_execution_role[each.key].name
2881
3406
  }
2882
3407
 
2883
3408
  # Additional IAM policies for Lambda (if provided)
2884
3409
  resource "aws_iam_role_policy" "lambda_additional_policies" {
2885
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
2886
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
2887
- role = aws_iam_role.lambda_execution_role.id
3410
+ for_each = local.operations
3411
+
3412
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
3413
+ role = aws_iam_role.lambda_execution_role[each.key].id
2888
3414
 
2889
3415
  policy = jsonencode({
2890
3416
  Version = "2012-10-17"
2891
- Statement = local.lambda_policy_statements
3417
+ Statement = local.lambda_policy_document_statements
2892
3418
  })
2893
3419
  }
2894
3420
 
@@ -2904,6 +3430,15 @@ locals {
2904
3430
  Resource = ["\${var.appconfig_application_arn}/*"]
2905
3431
  }
2906
3432
  ] : [], var.additional_iam_policy_statements)
3433
+
3434
+ # IAM rejects an empty statement list, so fall back to a no-op deny
3435
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
3436
+ {
3437
+ Effect = "Deny"
3438
+ Action = ["appconfig:GetLatestConfiguration"]
3439
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
3440
+ }
3441
+ ]
2907
3442
  }
2908
3443
 
2909
3444
  # CloudWatch Log Group for Lambda
@@ -2911,15 +3446,19 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
2911
3446
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2912
3447
  #checkov:skip=CKV_AWS_338:Log retention set to forever
2913
3448
  #checkov:skip=CKV_AWS_66:Log retention set to forever
2914
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
2915
- tags = var.tags
3449
+ for_each = local.operations
3450
+
3451
+ name = "/aws/lambda/\${local.function_name[each.key]}"
3452
+ tags = var.tags
2916
3453
  }
2917
3454
 
2918
3455
  # Lambda alias pointing to the latest published version for SnapStart
2919
3456
  resource "aws_lambda_alias" "live" {
3457
+ for_each = local.operations
3458
+
2920
3459
  name = "live"
2921
- function_name = aws_lambda_function.api_lambda.function_name
2922
- function_version = aws_lambda_function.api_lambda.version
3460
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3461
+ function_version = aws_lambda_function.api_lambda[each.key].version
2923
3462
 
2924
3463
  depends_on = [aws_lambda_function.api_lambda]
2925
3464
  }
@@ -2933,34 +3472,14 @@ resource "aws_api_gateway_authorizer" "cognito_authorizer" {
2933
3472
  identity_source = "method.request.header.Authorization"
2934
3473
  }
2935
3474
 
2936
- # Create proxy resource (captures all paths)
2937
- resource "aws_api_gateway_resource" "proxy_resource" {
2938
- rest_api_id = module.rest_api.api_id
2939
- parent_id = module.rest_api.api_root_resource_id
2940
- path_part = "{proxy+}"
2941
- }
2942
-
2943
- # Lambda integration for REST API
2944
- resource "aws_api_gateway_integration" "lambda_integration" {
2945
- rest_api_id = module.rest_api.api_id
2946
- resource_id = aws_api_gateway_resource.proxy_resource.id
2947
- http_method = aws_api_gateway_method.proxy_method.http_method
2948
-
2949
- integration_http_method = "POST"
2950
- type = "AWS_PROXY"
2951
- # Use the response streaming invocation path with the alias ARN for SnapStart
2952
- uri = "arn:aws:apigateway:\${data.aws_region.current.region}:lambda:path/2021-11-15/functions/\${aws_lambda_alias.live.arn}/response-streaming-invocations"
2953
- response_transfer_mode = "STREAM"
2954
-
2955
- depends_on = [aws_lambda_alias.live]
2956
- }
2957
-
2958
- # Method for proxy integration
2959
- resource "aws_api_gateway_method" "proxy_method" {
3475
+ # Method per operation
3476
+ resource "aws_api_gateway_method" "operation_methods" {
2960
3477
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
3478
+ for_each = local.operations
3479
+
2961
3480
  rest_api_id = module.rest_api.api_id
2962
- resource_id = aws_api_gateway_resource.proxy_resource.id
2963
- http_method = "ANY"
3481
+ resource_id = local.operation_resource_id[each.key]
3482
+ http_method = upper(each.value.method)
2964
3483
 
2965
3484
  authorization = "COGNITO_USER_POOLS"
2966
3485
  authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
@@ -2968,28 +3487,52 @@ resource "aws_api_gateway_method" "proxy_method" {
2968
3487
  # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
2969
3488
  authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
2970
3489
 
3490
+ # Path parameters must be declared on the method
2971
3491
  request_parameters = {
2972
- "method.request.path.proxy" = true
3492
+ for segment in local.operation_segments[each.key] :
3493
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
3494
+ if startswith(segment, "{")
2973
3495
  }
2974
3496
 
2975
3497
  depends_on = [aws_api_gateway_authorizer.cognito_authorizer]
2976
3498
  }
2977
3499
 
2978
- # OPTIONS method for CORS preflight
3500
+ # Lambda integration per operation
3501
+ resource "aws_api_gateway_integration" "lambda_integration" {
3502
+ for_each = local.operations
3503
+
3504
+ rest_api_id = module.rest_api.api_id
3505
+ resource_id = local.operation_resource_id[each.key]
3506
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
3507
+
3508
+ integration_http_method = "POST"
3509
+ type = "AWS_PROXY"
3510
+ # Use the response streaming invocation path with the alias ARN for SnapStart
3511
+ 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"
3512
+ response_transfer_mode = "STREAM"
3513
+
3514
+ depends_on = [aws_lambda_alias.live]
3515
+ }
3516
+
3517
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
2979
3518
  resource "aws_api_gateway_method" "options_method" {
2980
3519
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
2981
3520
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
3521
+ for_each = local.resource_ids
3522
+
2982
3523
  rest_api_id = module.rest_api.api_id
2983
- resource_id = aws_api_gateway_resource.proxy_resource.id
3524
+ resource_id = each.value
2984
3525
  http_method = "OPTIONS"
2985
3526
  authorization = "NONE"
2986
3527
  }
2987
3528
 
2988
- # CORS integration for OPTIONS method
3529
+ # CORS integration for OPTIONS methods
2989
3530
  resource "aws_api_gateway_integration" "options_integration" {
3531
+ for_each = local.resource_ids
3532
+
2990
3533
  rest_api_id = module.rest_api.api_id
2991
- resource_id = aws_api_gateway_resource.proxy_resource.id
2992
- http_method = aws_api_gateway_method.options_method.http_method
3534
+ resource_id = each.value
3535
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
2993
3536
 
2994
3537
  type = "MOCK"
2995
3538
  request_templates = {
@@ -2997,11 +3540,13 @@ resource "aws_api_gateway_integration" "options_integration" {
2997
3540
  }
2998
3541
  }
2999
3542
 
3000
- # OPTIONS method response
3543
+ # OPTIONS method responses
3001
3544
  resource "aws_api_gateway_method_response" "options_response" {
3545
+ for_each = local.resource_ids
3546
+
3002
3547
  rest_api_id = module.rest_api.api_id
3003
- resource_id = aws_api_gateway_resource.proxy_resource.id
3004
- http_method = aws_api_gateway_method.options_method.http_method
3548
+ resource_id = each.value
3549
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3005
3550
  status_code = "204"
3006
3551
 
3007
3552
  response_parameters = {
@@ -3011,18 +3556,22 @@ resource "aws_api_gateway_method_response" "options_response" {
3011
3556
  }
3012
3557
  }
3013
3558
 
3014
- # OPTIONS integration response
3559
+ # OPTIONS integration responses
3015
3560
  resource "aws_api_gateway_integration_response" "options_integration_response" {
3561
+ for_each = local.resource_ids
3562
+
3016
3563
  rest_api_id = module.rest_api.api_id
3017
- resource_id = aws_api_gateway_resource.proxy_resource.id
3018
- http_method = aws_api_gateway_method.options_method.http_method
3019
- status_code = aws_api_gateway_method_response.options_response.status_code
3564
+ resource_id = each.value
3565
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3566
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
3020
3567
 
3021
3568
  response_parameters = {
3022
3569
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
3023
3570
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
3024
3571
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
3025
3572
  }
3573
+
3574
+ depends_on = [aws_api_gateway_integration.options_integration]
3026
3575
  }
3027
3576
 
3028
3577
  # API Gateway deployment
@@ -3030,13 +3579,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3030
3579
  rest_api_id = module.rest_api.api_id
3031
3580
 
3032
3581
  triggers = {
3033
- redeployment = sha1(jsonencode([
3034
- aws_api_gateway_resource.proxy_resource.id,
3035
- aws_api_gateway_method.proxy_method.id,
3036
- aws_api_gateway_integration.lambda_integration.id,
3037
- aws_api_gateway_method.options_method.id,
3038
- aws_api_gateway_integration.options_integration.id,
3039
- ]))
3582
+ redeployment = sha1(jsonencode(concat(
3583
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
3584
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
3585
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
3586
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
3587
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
3588
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
3589
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
3590
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
3591
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
3592
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
3593
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
3594
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
3595
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
3596
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
3597
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
3598
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
3599
+ [for m in aws_api_gateway_method.operation_methods : m.id],
3600
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
3601
+ [for m in aws_api_gateway_method.options_method : m.id],
3602
+ [for i in aws_api_gateway_integration.options_integration : i.id],
3603
+ )))
3040
3604
  }
3041
3605
 
3042
3606
  lifecycle {
@@ -3044,7 +3608,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3044
3608
  }
3045
3609
 
3046
3610
  depends_on = [
3047
- aws_api_gateway_method.proxy_method,
3611
+ aws_api_gateway_method.operation_methods,
3048
3612
  aws_api_gateway_integration.lambda_integration,
3049
3613
  aws_api_gateway_method.options_method,
3050
3614
  aws_api_gateway_integration.options_integration,
@@ -3172,24 +3736,28 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
3172
3736
 
3173
3737
  # Lambda permission for API Gateway to invoke the function via alias
3174
3738
  resource "aws_lambda_permission" "api_gateway_invoke" {
3739
+ for_each = local.operations
3740
+
3175
3741
  statement_id = "AllowExecutionFromAPIGateway"
3176
3742
  action = "lambda:InvokeFunction"
3177
- function_name = aws_lambda_function.api_lambda.function_name
3178
- qualifier = aws_lambda_alias.live.name
3743
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3744
+ qualifier = aws_lambda_alias.live[each.key].name
3179
3745
  principal = "apigateway.amazonaws.com"
3180
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
3746
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
3181
3747
 
3182
3748
  depends_on = [module.rest_api, aws_lambda_alias.live]
3183
3749
  }
3184
3750
 
3185
3751
  # Lambda permission for API Gateway to invoke with response streaming via alias
3186
3752
  resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
3753
+ for_each = local.operations
3754
+
3187
3755
  statement_id = "AllowStreamingFromAPIGateway"
3188
3756
  action = "lambda:InvokeFunctionWithResponseStream"
3189
- function_name = aws_lambda_function.api_lambda.function_name
3190
- qualifier = aws_lambda_alias.live.name
3757
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3758
+ qualifier = aws_lambda_alias.live[each.key].name
3191
3759
  principal = "apigateway.amazonaws.com"
3192
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
3760
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
3193
3761
 
3194
3762
  depends_on = [module.rest_api, aws_lambda_alias.live]
3195
3763
  }
@@ -3200,7 +3768,7 @@ module "add_url_to_runtime_config" {
3200
3768
 
3201
3769
  namespace = "connection"
3202
3770
  key = "apis"
3203
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
3771
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
3204
3772
 
3205
3773
  depends_on = [aws_api_gateway_stage.api_stage]
3206
3774
  }
@@ -3253,83 +3821,83 @@ output "stage_id" {
3253
3821
  value = aws_api_gateway_stage.api_stage.id
3254
3822
  }
3255
3823
 
3256
- # Lambda Function Outputs
3257
- output "lambda_function_name" {
3258
- description = "Name of the Lambda function"
3259
- value = aws_lambda_function.api_lambda.function_name
3824
+ # Lambda Function Outputs, keyed by operation name
3825
+ output "operations" {
3826
+ description = "Names of the API operations, each with its own Lambda function"
3827
+ value = keys(local.operations)
3260
3828
  }
3261
3829
 
3262
- output "lambda_function_arn" {
3263
- description = "ARN of the Lambda function"
3264
- value = aws_lambda_function.api_lambda.arn
3830
+ output "lambda_function_names" {
3831
+ description = "Name of each operation's Lambda function, keyed by operation name"
3832
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
3265
3833
  }
3266
3834
 
3267
- output "lambda_invoke_arn" {
3268
- description = "Invoke ARN of the Lambda function"
3269
- value = aws_lambda_function.api_lambda.invoke_arn
3835
+ output "lambda_function_arns" {
3836
+ description = "ARN of each operation's Lambda function, keyed by operation name"
3837
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
3270
3838
  }
3271
3839
 
3272
- output "lambda_qualified_arn" {
3273
- description = "Qualified ARN of the Lambda function"
3274
- value = aws_lambda_function.api_lambda.qualified_arn
3840
+ output "lambda_invoke_arns" {
3841
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
3842
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
3275
3843
  }
3276
3844
 
3277
- output "lambda_version" {
3278
- description = "Version of the Lambda function"
3279
- value = aws_lambda_function.api_lambda.version
3845
+ output "lambda_qualified_arns" {
3846
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
3847
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
3280
3848
  }
3281
3849
 
3282
- output "lambda_source_code_hash" {
3283
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
3284
- value = aws_lambda_function.api_lambda.source_code_hash
3850
+ output "lambda_versions" {
3851
+ description = "Version of each operation's Lambda function, keyed by operation name"
3852
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
3285
3853
  }
3286
3854
 
3287
- output "lambda_source_code_size" {
3288
- description = "Size of the Lambda deployment package in bytes"
3289
- value = aws_lambda_function.api_lambda.source_code_size
3855
+ output "lambda_source_code_hash" {
3856
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
3857
+ value = data.archive_file.lambda_zip.output_base64sha256
3290
3858
  }
3291
3859
 
3292
- # IAM Role Outputs
3293
- output "lambda_execution_role_arn" {
3294
- description = "ARN of the Lambda execution role"
3295
- value = aws_iam_role.lambda_execution_role.arn
3860
+ # IAM Role Outputs, keyed by operation name
3861
+ output "lambda_execution_role_arns" {
3862
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
3863
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
3296
3864
  }
3297
3865
 
3298
- output "lambda_execution_role_name" {
3299
- description = "Name of the Lambda execution role"
3300
- value = aws_iam_role.lambda_execution_role.name
3866
+ output "lambda_execution_role_names" {
3867
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
3868
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
3301
3869
  }
3302
3870
 
3303
3871
  output "security_group_id" {
3304
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
3872
+ 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."
3305
3873
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
3306
3874
  }
3307
3875
 
3308
- # Integration Outputs
3309
- output "integration_id" {
3310
- description = "ID of the Lambda integration"
3311
- value = aws_api_gateway_integration.lambda_integration.id
3876
+ # Integration Outputs, keyed by operation name
3877
+ output "integration_ids" {
3878
+ description = "ID of each operation's Lambda integration, keyed by operation name"
3879
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
3312
3880
  }
3313
3881
 
3314
- output "proxy_resource_id" {
3315
- description = "ID of the proxy resource"
3316
- value = aws_api_gateway_resource.proxy_resource.id
3882
+ output "method_ids" {
3883
+ description = "ID of each operation's API Gateway method, keyed by operation name"
3884
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
3317
3885
  }
3318
3886
 
3319
- output "proxy_method_id" {
3320
- description = "ID of the proxy method"
3321
- value = aws_api_gateway_method.proxy_method.id
3887
+ output "resource_ids" {
3888
+ description = "ID of each API Gateway resource, keyed by its path"
3889
+ value = local.resource_ids
3322
3890
  }
3323
3891
 
3324
- # CloudWatch Log Groups
3325
- output "lambda_log_group_name" {
3326
- description = "Name of the Lambda CloudWatch log group"
3327
- value = aws_cloudwatch_log_group.lambda_logs.name
3892
+ # CloudWatch Log Groups, keyed by operation name
3893
+ output "lambda_log_group_names" {
3894
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
3895
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
3328
3896
  }
3329
3897
 
3330
- output "lambda_log_group_arn" {
3331
- description = "ARN of the Lambda CloudWatch log group"
3332
- value = aws_cloudwatch_log_group.lambda_logs.arn
3898
+ output "lambda_log_group_arns" {
3899
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
3900
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
3333
3901
  }
3334
3902
  ",
3335
3903
  }
@@ -3539,22 +4107,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
3539
4107
  }
3540
4108
  }
3541
4109
 
4110
+ resource "random_id" "waf_log_suffix" {
4111
+ count = var.enable_waf ? 1 : 0
4112
+ byte_length = 4
4113
+ }
4114
+
4115
+ locals {
4116
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
4117
+ }
4118
+
4119
+ # KMS key for encrypting WAF request logs at rest
4120
+ resource "aws_kms_key" "waf_logs" {
4121
+ count = var.enable_waf ? 1 : 0
4122
+
4123
+ description = "\${var.api_name} API WAF log encryption"
4124
+ deletion_window_in_days = 7
4125
+ enable_key_rotation = true
4126
+
4127
+ policy = jsonencode({
4128
+ Version = "2012-10-17"
4129
+ Statement = [
4130
+ {
4131
+ Sid = "Enable IAM User Permissions"
4132
+ Effect = "Allow"
4133
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
4134
+ Action = "kms:*"
4135
+ Resource = "*"
4136
+ },
4137
+ {
4138
+ Sid = "Allow CloudWatch Logs"
4139
+ Effect = "Allow"
4140
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
4141
+ Action = [
4142
+ "kms:Encrypt",
4143
+ "kms:Decrypt",
4144
+ "kms:ReEncrypt*",
4145
+ "kms:GenerateDataKey*",
4146
+ "kms:DescribeKey"
4147
+ ]
4148
+ Resource = "*"
4149
+ Condition = {
4150
+ ArnEquals = {
4151
+ "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}"
4152
+ }
4153
+ }
4154
+ }
4155
+ ]
4156
+ })
4157
+
4158
+ tags = var.tags
4159
+ }
4160
+
4161
+ resource "aws_kms_alias" "waf_logs" {
4162
+ count = var.enable_waf ? 1 : 0
4163
+
4164
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
4165
+ target_key_id = aws_kms_key.waf_logs[0].key_id
4166
+ }
4167
+
3542
4168
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
3543
4169
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
3544
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3545
4170
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
3546
4171
  count = var.enable_waf ? 1 : 0
3547
4172
 
3548
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
4173
+ name = local.waf_log_group_name
3549
4174
  retention_in_days = 30
4175
+ kms_key_id = aws_kms_key.waf_logs[0].arn
3550
4176
  tags = var.tags
3551
4177
  }
3552
4178
 
3553
- resource "random_id" "waf_log_suffix" {
3554
- count = var.enable_waf ? 1 : 0
3555
- byte_length = 4
3556
- }
3557
-
3558
4179
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
3559
4180
  count = var.enable_waf ? 1 : 0
3560
4181
 
@@ -3600,10 +4221,18 @@ output "waf_web_acl_arn" {
3600
4221
  required_version = ">= 1.0"
3601
4222
 
3602
4223
  required_providers {
4224
+ archive = {
4225
+ source = "hashicorp/archive"
4226
+ version = "2.8.0"
4227
+ }
3603
4228
  aws = {
3604
4229
  source = "hashicorp/aws"
3605
4230
  version = "6.61.0"
3606
4231
  }
4232
+ random = {
4233
+ source = "hashicorp/random"
4234
+ version = "3.9.0"
4235
+ }
3607
4236
  }
3608
4237
  }
3609
4238
 
@@ -3699,14 +4328,243 @@ variable "tags" {
3699
4328
  default = {}
3700
4329
  }
3701
4330
 
3702
- # Get current AWS region and account ID
3703
- data "aws_region" "current" {}
3704
- data "aws_caller_identity" "current" {}
4331
+ # Get current AWS region and account ID
4332
+ data "aws_region" "current" {}
4333
+ data "aws_caller_identity" "current" {}
4334
+
4335
+ resource "random_string" "suffix" {
4336
+ length = 8
4337
+ special = false
4338
+ upper = false
4339
+ }
4340
+
4341
+ locals {
4342
+ # Generated at build time from the API definition
4343
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
4344
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
4345
+
4346
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
4347
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
4348
+
4349
+ function_name = { for op in keys(local.operations) : op =>
4350
+ "\${substr("TestApi-\${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}"
4351
+ }
4352
+ role_name = { for op in keys(local.operations) : op =>
4353
+ "\${substr("TestApi-\${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}"
4354
+ }
4355
+
4356
+ # A REST API needs a resource per path segment, so each operation's path is
4357
+ # split into the distinct path prefixes the resource tree is built from below
4358
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
4359
+
4360
+ path_nodes = merge([
4361
+ for op, segments in local.operation_segments : {
4362
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
4363
+ depth = i
4364
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
4365
+ part = segment
4366
+ }
4367
+ }
4368
+ ]...)
4369
+
4370
+ # Instances of a resource cannot reference one another, so each depth is a
4371
+ # separate resource. Deeper paths are reported by the precondition below.
4372
+ max_path_depth = 16
4373
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
4374
+ for path, node in local.path_nodes : path => node if node.depth == depth
4375
+ } }
4376
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
4377
+
4378
+ resource_ids = merge(
4379
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
4380
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
4381
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
4382
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
4383
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
4384
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
4385
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
4386
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
4387
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
4388
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
4389
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
4390
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
4391
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
4392
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
4393
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
4394
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
4395
+ )
4396
+
4397
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
4398
+ local.resource_ids[join("/", segments)]
4399
+ }
4400
+
4401
+ permission_source_suffix = { for op, details in local.operations : op =>
4402
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
4403
+ }
4404
+ }
4405
+
4406
+ resource "terraform_data" "operations_metadata" {
4407
+ # Fails the plan when the operations metadata has not been generated
4408
+ input = local.operations_file
4409
+
4410
+ lifecycle {
4411
+ precondition {
4412
+ condition = fileexists(local.operations_file)
4413
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
4414
+ }
4415
+ precondition {
4416
+ condition = length(local.operations) > 0
4417
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
4418
+ }
4419
+ precondition {
4420
+ condition = length(local.paths_too_deep) == 0
4421
+ 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."
4422
+ }
4423
+ }
4424
+ }
4425
+
4426
+ # API Gateway resources at path depth 0
4427
+ resource "aws_api_gateway_resource" "path_depth_0" {
4428
+ for_each = local.path_nodes_by_depth[0]
4429
+
4430
+ rest_api_id = module.rest_api.api_id
4431
+ parent_id = module.rest_api.api_root_resource_id
4432
+ path_part = each.value.part
4433
+ }
4434
+
4435
+ # API Gateway resources at path depth 1
4436
+ resource "aws_api_gateway_resource" "path_depth_1" {
4437
+ for_each = local.path_nodes_by_depth[1]
4438
+
4439
+ rest_api_id = module.rest_api.api_id
4440
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
4441
+ path_part = each.value.part
4442
+ }
4443
+
4444
+ # API Gateway resources at path depth 2
4445
+ resource "aws_api_gateway_resource" "path_depth_2" {
4446
+ for_each = local.path_nodes_by_depth[2]
4447
+
4448
+ rest_api_id = module.rest_api.api_id
4449
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
4450
+ path_part = each.value.part
4451
+ }
4452
+
4453
+ # API Gateway resources at path depth 3
4454
+ resource "aws_api_gateway_resource" "path_depth_3" {
4455
+ for_each = local.path_nodes_by_depth[3]
4456
+
4457
+ rest_api_id = module.rest_api.api_id
4458
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
4459
+ path_part = each.value.part
4460
+ }
4461
+
4462
+ # API Gateway resources at path depth 4
4463
+ resource "aws_api_gateway_resource" "path_depth_4" {
4464
+ for_each = local.path_nodes_by_depth[4]
4465
+
4466
+ rest_api_id = module.rest_api.api_id
4467
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
4468
+ path_part = each.value.part
4469
+ }
4470
+
4471
+ # API Gateway resources at path depth 5
4472
+ resource "aws_api_gateway_resource" "path_depth_5" {
4473
+ for_each = local.path_nodes_by_depth[5]
4474
+
4475
+ rest_api_id = module.rest_api.api_id
4476
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
4477
+ path_part = each.value.part
4478
+ }
4479
+
4480
+ # API Gateway resources at path depth 6
4481
+ resource "aws_api_gateway_resource" "path_depth_6" {
4482
+ for_each = local.path_nodes_by_depth[6]
4483
+
4484
+ rest_api_id = module.rest_api.api_id
4485
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
4486
+ path_part = each.value.part
4487
+ }
4488
+
4489
+ # API Gateway resources at path depth 7
4490
+ resource "aws_api_gateway_resource" "path_depth_7" {
4491
+ for_each = local.path_nodes_by_depth[7]
4492
+
4493
+ rest_api_id = module.rest_api.api_id
4494
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
4495
+ path_part = each.value.part
4496
+ }
4497
+
4498
+ # API Gateway resources at path depth 8
4499
+ resource "aws_api_gateway_resource" "path_depth_8" {
4500
+ for_each = local.path_nodes_by_depth[8]
4501
+
4502
+ rest_api_id = module.rest_api.api_id
4503
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
4504
+ path_part = each.value.part
4505
+ }
4506
+
4507
+ # API Gateway resources at path depth 9
4508
+ resource "aws_api_gateway_resource" "path_depth_9" {
4509
+ for_each = local.path_nodes_by_depth[9]
4510
+
4511
+ rest_api_id = module.rest_api.api_id
4512
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
4513
+ path_part = each.value.part
4514
+ }
4515
+
4516
+ # API Gateway resources at path depth 10
4517
+ resource "aws_api_gateway_resource" "path_depth_10" {
4518
+ for_each = local.path_nodes_by_depth[10]
4519
+
4520
+ rest_api_id = module.rest_api.api_id
4521
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
4522
+ path_part = each.value.part
4523
+ }
4524
+
4525
+ # API Gateway resources at path depth 11
4526
+ resource "aws_api_gateway_resource" "path_depth_11" {
4527
+ for_each = local.path_nodes_by_depth[11]
4528
+
4529
+ rest_api_id = module.rest_api.api_id
4530
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
4531
+ path_part = each.value.part
4532
+ }
4533
+
4534
+ # API Gateway resources at path depth 12
4535
+ resource "aws_api_gateway_resource" "path_depth_12" {
4536
+ for_each = local.path_nodes_by_depth[12]
4537
+
4538
+ rest_api_id = module.rest_api.api_id
4539
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
4540
+ path_part = each.value.part
4541
+ }
4542
+
4543
+ # API Gateway resources at path depth 13
4544
+ resource "aws_api_gateway_resource" "path_depth_13" {
4545
+ for_each = local.path_nodes_by_depth[13]
4546
+
4547
+ rest_api_id = module.rest_api.api_id
4548
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
4549
+ path_part = each.value.part
4550
+ }
4551
+
4552
+ # API Gateway resources at path depth 14
4553
+ resource "aws_api_gateway_resource" "path_depth_14" {
4554
+ for_each = local.path_nodes_by_depth[14]
3705
4555
 
3706
- resource "random_string" "suffix" {
3707
- length = 8
3708
- special = false
3709
- upper = false
4556
+ rest_api_id = module.rest_api.api_id
4557
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
4558
+ path_part = each.value.part
4559
+ }
4560
+
4561
+ # API Gateway resources at path depth 15
4562
+ resource "aws_api_gateway_resource" "path_depth_15" {
4563
+ for_each = local.path_nodes_by_depth[15]
4564
+
4565
+ rest_api_id = module.rest_api.api_id
4566
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
4567
+ path_part = each.value.part
3710
4568
  }
3711
4569
 
3712
4570
  # Resources
@@ -3783,8 +4641,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
3783
4641
  description = "Allow outbound HTTPS to AWS service endpoints"
3784
4642
  }
3785
4643
 
3786
- # Lambda function
3787
4644
  resource "aws_lambda_function" "api_lambda" {
4645
+ # One lambda function per operation, each serving just that operation
4646
+ for_each = local.operations
4647
+
3788
4648
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
3789
4649
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
3790
4650
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -3793,8 +4653,8 @@ resource "aws_lambda_function" "api_lambda" {
3793
4653
  s3_bucket = aws_s3_object.lambda_zip.bucket
3794
4654
  s3_key = aws_s3_object.lambda_zip.key
3795
4655
  s3_object_version = aws_s3_object.lambda_zip.version_id
3796
- function_name = "TestApiHandler-\${random_string.suffix.result}"
3797
- role = aws_iam_role.lambda_execution_role.arn
4656
+ function_name = local.function_name[each.key]
4657
+ role = aws_iam_role.lambda_execution_role[each.key].arn
3798
4658
  handler = "run.sh"
3799
4659
  runtime = "python3.14"
3800
4660
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -3839,7 +4699,9 @@ resource "aws_lambda_function" "api_lambda" {
3839
4699
 
3840
4700
  # IAM role for Lambda execution
3841
4701
  resource "aws_iam_role" "lambda_execution_role" {
3842
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
4702
+ for_each = local.operations
4703
+
4704
+ name = local.role_name[each.key]
3843
4705
 
3844
4706
  assume_role_policy = jsonencode({
3845
4707
  Version = "2012-10-17"
@@ -3859,32 +4721,38 @@ resource "aws_iam_role" "lambda_execution_role" {
3859
4721
 
3860
4722
  # Attach basic execution policy to Lambda role
3861
4723
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
4724
+ for_each = local.operations
4725
+
3862
4726
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
3863
- role = aws_iam_role.lambda_execution_role.name
4727
+ role = aws_iam_role.lambda_execution_role[each.key].name
3864
4728
  }
3865
4729
 
3866
4730
  # Attach X-Ray tracing policy to Lambda role
3867
4731
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
4732
+ for_each = local.operations
4733
+
3868
4734
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
3869
- role = aws_iam_role.lambda_execution_role.name
4735
+ role = aws_iam_role.lambda_execution_role[each.key].name
3870
4736
  }
3871
4737
 
3872
4738
  # Attach VPC access policy to Lambda role when deployed into a VPC
3873
4739
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
3874
- count = var.enable_vpc ? 1 : 0
4740
+ for_each = var.enable_vpc ? local.operations : {}
4741
+
3875
4742
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
3876
- role = aws_iam_role.lambda_execution_role.name
4743
+ role = aws_iam_role.lambda_execution_role[each.key].name
3877
4744
  }
3878
4745
 
3879
4746
  # Additional IAM policies for Lambda (if provided)
3880
4747
  resource "aws_iam_role_policy" "lambda_additional_policies" {
3881
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
3882
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
3883
- role = aws_iam_role.lambda_execution_role.id
4748
+ for_each = local.operations
4749
+
4750
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
4751
+ role = aws_iam_role.lambda_execution_role[each.key].id
3884
4752
 
3885
4753
  policy = jsonencode({
3886
4754
  Version = "2012-10-17"
3887
- Statement = local.lambda_policy_statements
4755
+ Statement = local.lambda_policy_document_statements
3888
4756
  })
3889
4757
  }
3890
4758
 
@@ -3900,6 +4768,15 @@ locals {
3900
4768
  Resource = ["\${var.appconfig_application_arn}/*"]
3901
4769
  }
3902
4770
  ] : [], var.additional_iam_policy_statements)
4771
+
4772
+ # IAM rejects an empty statement list, so fall back to a no-op deny
4773
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
4774
+ {
4775
+ Effect = "Deny"
4776
+ Action = ["appconfig:GetLatestConfiguration"]
4777
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
4778
+ }
4779
+ ]
3903
4780
  }
3904
4781
 
3905
4782
  # CloudWatch Log Group for Lambda
@@ -3907,73 +4784,81 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
3907
4784
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3908
4785
  #checkov:skip=CKV_AWS_338:Log retention set to forever
3909
4786
  #checkov:skip=CKV_AWS_66:Log retention set to forever
3910
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
3911
- tags = var.tags
4787
+ for_each = local.operations
4788
+
4789
+ name = "/aws/lambda/\${local.function_name[each.key]}"
4790
+ tags = var.tags
3912
4791
  }
3913
4792
 
3914
4793
  # Lambda alias pointing to the latest published version for SnapStart
3915
4794
  resource "aws_lambda_alias" "live" {
4795
+ for_each = local.operations
4796
+
3916
4797
  name = "live"
3917
- function_name = aws_lambda_function.api_lambda.function_name
3918
- function_version = aws_lambda_function.api_lambda.version
4798
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
4799
+ function_version = aws_lambda_function.api_lambda[each.key].version
3919
4800
 
3920
4801
  depends_on = [aws_lambda_function.api_lambda]
3921
4802
  }
3922
4803
 
3923
4804
 
3924
- # Create proxy resource (captures all paths)
3925
- resource "aws_api_gateway_resource" "proxy_resource" {
3926
- rest_api_id = module.rest_api.api_id
3927
- parent_id = module.rest_api.api_root_resource_id
3928
- path_part = "{proxy+}"
4805
+ # Method per operation
4806
+ resource "aws_api_gateway_method" "operation_methods" {
4807
+ #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
4808
+ for_each = local.operations
4809
+
4810
+ rest_api_id = module.rest_api.api_id
4811
+ resource_id = local.operation_resource_id[each.key]
4812
+ http_method = upper(each.value.method)
4813
+
4814
+ authorization = "AWS_IAM"
4815
+
4816
+ # Path parameters must be declared on the method
4817
+ request_parameters = {
4818
+ for segment in local.operation_segments[each.key] :
4819
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
4820
+ if startswith(segment, "{")
4821
+ }
4822
+
4823
+ depends_on = []
3929
4824
  }
3930
4825
 
3931
- # Lambda integration for REST API
4826
+ # Lambda integration per operation
3932
4827
  resource "aws_api_gateway_integration" "lambda_integration" {
4828
+ for_each = local.operations
4829
+
3933
4830
  rest_api_id = module.rest_api.api_id
3934
- resource_id = aws_api_gateway_resource.proxy_resource.id
3935
- http_method = aws_api_gateway_method.proxy_method.http_method
4831
+ resource_id = local.operation_resource_id[each.key]
4832
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
3936
4833
 
3937
4834
  integration_http_method = "POST"
3938
4835
  type = "AWS_PROXY"
3939
4836
  # Use the response streaming invocation path with the alias ARN for SnapStart
3940
- uri = "arn:aws:apigateway:\${data.aws_region.current.region}:lambda:path/2021-11-15/functions/\${aws_lambda_alias.live.arn}/response-streaming-invocations"
4837
+ 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"
3941
4838
  response_transfer_mode = "STREAM"
3942
4839
 
3943
4840
  depends_on = [aws_lambda_alias.live]
3944
4841
  }
3945
4842
 
3946
- # Method for proxy integration
3947
- resource "aws_api_gateway_method" "proxy_method" {
3948
- #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
3949
- rest_api_id = module.rest_api.api_id
3950
- resource_id = aws_api_gateway_resource.proxy_resource.id
3951
- http_method = "ANY"
3952
-
3953
- authorization = "AWS_IAM"
3954
-
3955
- request_parameters = {
3956
- "method.request.path.proxy" = true
3957
- }
3958
-
3959
- depends_on = []
3960
- }
3961
-
3962
- # OPTIONS method for CORS preflight
4843
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
3963
4844
  resource "aws_api_gateway_method" "options_method" {
3964
4845
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
3965
4846
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
4847
+ for_each = local.resource_ids
4848
+
3966
4849
  rest_api_id = module.rest_api.api_id
3967
- resource_id = aws_api_gateway_resource.proxy_resource.id
4850
+ resource_id = each.value
3968
4851
  http_method = "OPTIONS"
3969
4852
  authorization = "NONE"
3970
4853
  }
3971
4854
 
3972
- # CORS integration for OPTIONS method
4855
+ # CORS integration for OPTIONS methods
3973
4856
  resource "aws_api_gateway_integration" "options_integration" {
4857
+ for_each = local.resource_ids
4858
+
3974
4859
  rest_api_id = module.rest_api.api_id
3975
- resource_id = aws_api_gateway_resource.proxy_resource.id
3976
- http_method = aws_api_gateway_method.options_method.http_method
4860
+ resource_id = each.value
4861
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3977
4862
 
3978
4863
  type = "MOCK"
3979
4864
  request_templates = {
@@ -3981,11 +4866,13 @@ resource "aws_api_gateway_integration" "options_integration" {
3981
4866
  }
3982
4867
  }
3983
4868
 
3984
- # OPTIONS method response
4869
+ # OPTIONS method responses
3985
4870
  resource "aws_api_gateway_method_response" "options_response" {
4871
+ for_each = local.resource_ids
4872
+
3986
4873
  rest_api_id = module.rest_api.api_id
3987
- resource_id = aws_api_gateway_resource.proxy_resource.id
3988
- http_method = aws_api_gateway_method.options_method.http_method
4874
+ resource_id = each.value
4875
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3989
4876
  status_code = "204"
3990
4877
 
3991
4878
  response_parameters = {
@@ -3995,18 +4882,22 @@ resource "aws_api_gateway_method_response" "options_response" {
3995
4882
  }
3996
4883
  }
3997
4884
 
3998
- # OPTIONS integration response
4885
+ # OPTIONS integration responses
3999
4886
  resource "aws_api_gateway_integration_response" "options_integration_response" {
4887
+ for_each = local.resource_ids
4888
+
4000
4889
  rest_api_id = module.rest_api.api_id
4001
- resource_id = aws_api_gateway_resource.proxy_resource.id
4002
- http_method = aws_api_gateway_method.options_method.http_method
4003
- status_code = aws_api_gateway_method_response.options_response.status_code
4890
+ resource_id = each.value
4891
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
4892
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
4004
4893
 
4005
4894
  response_parameters = {
4006
4895
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
4007
4896
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
4008
4897
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
4009
4898
  }
4899
+
4900
+ depends_on = [aws_api_gateway_integration.options_integration]
4010
4901
  }
4011
4902
 
4012
4903
  # API Gateway deployment
@@ -4014,13 +4905,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4014
4905
  rest_api_id = module.rest_api.api_id
4015
4906
 
4016
4907
  triggers = {
4017
- redeployment = sha1(jsonencode([
4018
- aws_api_gateway_resource.proxy_resource.id,
4019
- aws_api_gateway_method.proxy_method.id,
4020
- aws_api_gateway_integration.lambda_integration.id,
4021
- aws_api_gateway_method.options_method.id,
4022
- aws_api_gateway_integration.options_integration.id,
4023
- ]))
4908
+ redeployment = sha1(jsonencode(concat(
4909
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
4910
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
4911
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
4912
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
4913
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
4914
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
4915
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
4916
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
4917
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
4918
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
4919
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
4920
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
4921
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
4922
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
4923
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
4924
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
4925
+ [for m in aws_api_gateway_method.operation_methods : m.id],
4926
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
4927
+ [for m in aws_api_gateway_method.options_method : m.id],
4928
+ [for i in aws_api_gateway_integration.options_integration : i.id],
4929
+ )))
4024
4930
  }
4025
4931
 
4026
4932
  lifecycle {
@@ -4028,7 +4934,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4028
4934
  }
4029
4935
 
4030
4936
  depends_on = [
4031
- aws_api_gateway_method.proxy_method,
4937
+ aws_api_gateway_method.operation_methods,
4032
4938
  aws_api_gateway_integration.lambda_integration,
4033
4939
  aws_api_gateway_method.options_method,
4034
4940
  aws_api_gateway_integration.options_integration,
@@ -4167,24 +5073,28 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
4167
5073
 
4168
5074
  # Lambda permission for API Gateway to invoke the function via alias
4169
5075
  resource "aws_lambda_permission" "api_gateway_invoke" {
5076
+ for_each = local.operations
5077
+
4170
5078
  statement_id = "AllowExecutionFromAPIGateway"
4171
5079
  action = "lambda:InvokeFunction"
4172
- function_name = aws_lambda_function.api_lambda.function_name
4173
- qualifier = aws_lambda_alias.live.name
5080
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
5081
+ qualifier = aws_lambda_alias.live[each.key].name
4174
5082
  principal = "apigateway.amazonaws.com"
4175
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
5083
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
4176
5084
 
4177
5085
  depends_on = [module.rest_api, aws_lambda_alias.live]
4178
5086
  }
4179
5087
 
4180
5088
  # Lambda permission for API Gateway to invoke with response streaming via alias
4181
5089
  resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
5090
+ for_each = local.operations
5091
+
4182
5092
  statement_id = "AllowStreamingFromAPIGateway"
4183
5093
  action = "lambda:InvokeFunctionWithResponseStream"
4184
- function_name = aws_lambda_function.api_lambda.function_name
4185
- qualifier = aws_lambda_alias.live.name
5094
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
5095
+ qualifier = aws_lambda_alias.live[each.key].name
4186
5096
  principal = "apigateway.amazonaws.com"
4187
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
5097
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
4188
5098
 
4189
5099
  depends_on = [module.rest_api, aws_lambda_alias.live]
4190
5100
  }
@@ -4195,7 +5105,7 @@ module "add_url_to_runtime_config" {
4195
5105
 
4196
5106
  namespace = "connection"
4197
5107
  key = "apis"
4198
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
5108
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
4199
5109
 
4200
5110
  depends_on = [aws_api_gateway_stage.api_stage]
4201
5111
  }
@@ -4248,83 +5158,83 @@ output "stage_id" {
4248
5158
  value = aws_api_gateway_stage.api_stage.id
4249
5159
  }
4250
5160
 
4251
- # Lambda Function Outputs
4252
- output "lambda_function_name" {
4253
- description = "Name of the Lambda function"
4254
- value = aws_lambda_function.api_lambda.function_name
5161
+ # Lambda Function Outputs, keyed by operation name
5162
+ output "operations" {
5163
+ description = "Names of the API operations, each with its own Lambda function"
5164
+ value = keys(local.operations)
4255
5165
  }
4256
5166
 
4257
- output "lambda_function_arn" {
4258
- description = "ARN of the Lambda function"
4259
- value = aws_lambda_function.api_lambda.arn
5167
+ output "lambda_function_names" {
5168
+ description = "Name of each operation's Lambda function, keyed by operation name"
5169
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
4260
5170
  }
4261
5171
 
4262
- output "lambda_invoke_arn" {
4263
- description = "Invoke ARN of the Lambda function"
4264
- value = aws_lambda_function.api_lambda.invoke_arn
5172
+ output "lambda_function_arns" {
5173
+ description = "ARN of each operation's Lambda function, keyed by operation name"
5174
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
4265
5175
  }
4266
5176
 
4267
- output "lambda_qualified_arn" {
4268
- description = "Qualified ARN of the Lambda function"
4269
- value = aws_lambda_function.api_lambda.qualified_arn
5177
+ output "lambda_invoke_arns" {
5178
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
5179
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
4270
5180
  }
4271
5181
 
4272
- output "lambda_version" {
4273
- description = "Version of the Lambda function"
4274
- value = aws_lambda_function.api_lambda.version
5182
+ output "lambda_qualified_arns" {
5183
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
5184
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
4275
5185
  }
4276
5186
 
4277
- output "lambda_source_code_hash" {
4278
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
4279
- value = aws_lambda_function.api_lambda.source_code_hash
5187
+ output "lambda_versions" {
5188
+ description = "Version of each operation's Lambda function, keyed by operation name"
5189
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
4280
5190
  }
4281
5191
 
4282
- output "lambda_source_code_size" {
4283
- description = "Size of the Lambda deployment package in bytes"
4284
- value = aws_lambda_function.api_lambda.source_code_size
5192
+ output "lambda_source_code_hash" {
5193
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
5194
+ value = data.archive_file.lambda_zip.output_base64sha256
4285
5195
  }
4286
5196
 
4287
- # IAM Role Outputs
4288
- output "lambda_execution_role_arn" {
4289
- description = "ARN of the Lambda execution role"
4290
- value = aws_iam_role.lambda_execution_role.arn
5197
+ # IAM Role Outputs, keyed by operation name
5198
+ output "lambda_execution_role_arns" {
5199
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
5200
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
4291
5201
  }
4292
5202
 
4293
- output "lambda_execution_role_name" {
4294
- description = "Name of the Lambda execution role"
4295
- value = aws_iam_role.lambda_execution_role.name
5203
+ output "lambda_execution_role_names" {
5204
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
5205
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
4296
5206
  }
4297
5207
 
4298
5208
  output "security_group_id" {
4299
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
5209
+ 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."
4300
5210
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
4301
5211
  }
4302
5212
 
4303
- # Integration Outputs
4304
- output "integration_id" {
4305
- description = "ID of the Lambda integration"
4306
- value = aws_api_gateway_integration.lambda_integration.id
5213
+ # Integration Outputs, keyed by operation name
5214
+ output "integration_ids" {
5215
+ description = "ID of each operation's Lambda integration, keyed by operation name"
5216
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
4307
5217
  }
4308
5218
 
4309
- output "proxy_resource_id" {
4310
- description = "ID of the proxy resource"
4311
- value = aws_api_gateway_resource.proxy_resource.id
5219
+ output "method_ids" {
5220
+ description = "ID of each operation's API Gateway method, keyed by operation name"
5221
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
4312
5222
  }
4313
5223
 
4314
- output "proxy_method_id" {
4315
- description = "ID of the proxy method"
4316
- value = aws_api_gateway_method.proxy_method.id
5224
+ output "resource_ids" {
5225
+ description = "ID of each API Gateway resource, keyed by its path"
5226
+ value = local.resource_ids
4317
5227
  }
4318
5228
 
4319
- # CloudWatch Log Groups
4320
- output "lambda_log_group_name" {
4321
- description = "Name of the Lambda CloudWatch log group"
4322
- value = aws_cloudwatch_log_group.lambda_logs.name
5229
+ # CloudWatch Log Groups, keyed by operation name
5230
+ output "lambda_log_group_names" {
5231
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
5232
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
4323
5233
  }
4324
5234
 
4325
- output "lambda_log_group_arn" {
4326
- description = "ARN of the Lambda CloudWatch log group"
4327
- value = aws_cloudwatch_log_group.lambda_logs.arn
5235
+ output "lambda_log_group_arns" {
5236
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
5237
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
4328
5238
  }
4329
5239
  ",
4330
5240
  }
@@ -4534,22 +5444,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
4534
5444
  }
4535
5445
  }
4536
5446
 
5447
+ resource "random_id" "waf_log_suffix" {
5448
+ count = var.enable_waf ? 1 : 0
5449
+ byte_length = 4
5450
+ }
5451
+
5452
+ locals {
5453
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
5454
+ }
5455
+
5456
+ # KMS key for encrypting WAF request logs at rest
5457
+ resource "aws_kms_key" "waf_logs" {
5458
+ count = var.enable_waf ? 1 : 0
5459
+
5460
+ description = "\${var.api_name} API WAF log encryption"
5461
+ deletion_window_in_days = 7
5462
+ enable_key_rotation = true
5463
+
5464
+ policy = jsonencode({
5465
+ Version = "2012-10-17"
5466
+ Statement = [
5467
+ {
5468
+ Sid = "Enable IAM User Permissions"
5469
+ Effect = "Allow"
5470
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
5471
+ Action = "kms:*"
5472
+ Resource = "*"
5473
+ },
5474
+ {
5475
+ Sid = "Allow CloudWatch Logs"
5476
+ Effect = "Allow"
5477
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
5478
+ Action = [
5479
+ "kms:Encrypt",
5480
+ "kms:Decrypt",
5481
+ "kms:ReEncrypt*",
5482
+ "kms:GenerateDataKey*",
5483
+ "kms:DescribeKey"
5484
+ ]
5485
+ Resource = "*"
5486
+ Condition = {
5487
+ ArnEquals = {
5488
+ "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}"
5489
+ }
5490
+ }
5491
+ }
5492
+ ]
5493
+ })
5494
+
5495
+ tags = var.tags
5496
+ }
5497
+
5498
+ resource "aws_kms_alias" "waf_logs" {
5499
+ count = var.enable_waf ? 1 : 0
5500
+
5501
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
5502
+ target_key_id = aws_kms_key.waf_logs[0].key_id
5503
+ }
5504
+
4537
5505
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
4538
5506
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
4539
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
4540
5507
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
4541
5508
  count = var.enable_waf ? 1 : 0
4542
5509
 
4543
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
5510
+ name = local.waf_log_group_name
4544
5511
  retention_in_days = 30
5512
+ kms_key_id = aws_kms_key.waf_logs[0].arn
4545
5513
  tags = var.tags
4546
5514
  }
4547
5515
 
4548
- resource "random_id" "waf_log_suffix" {
4549
- count = var.enable_waf ? 1 : 0
4550
- byte_length = 4
4551
- }
4552
-
4553
5516
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
4554
5517
  count = var.enable_waf ? 1 : 0
4555
5518
 
@@ -4595,10 +5558,18 @@ output "waf_web_acl_arn" {
4595
5558
  required_version = ">= 1.0"
4596
5559
 
4597
5560
  required_providers {
5561
+ archive = {
5562
+ source = "hashicorp/archive"
5563
+ version = "2.8.0"
5564
+ }
4598
5565
  aws = {
4599
5566
  source = "hashicorp/aws"
4600
5567
  version = "6.61.0"
4601
5568
  }
5569
+ random = {
5570
+ source = "hashicorp/random"
5571
+ version = "3.9.0"
5572
+ }
4602
5573
  }
4603
5574
  }
4604
5575
 
@@ -4704,6 +5675,235 @@ resource "random_string" "suffix" {
4704
5675
  upper = false
4705
5676
  }
4706
5677
 
5678
+ locals {
5679
+ # Generated at build time from the API definition
5680
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
5681
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
5682
+
5683
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
5684
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
5685
+
5686
+ function_name = { for op in keys(local.operations) : op =>
5687
+ "\${substr("TestApi-\${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}"
5688
+ }
5689
+ role_name = { for op in keys(local.operations) : op =>
5690
+ "\${substr("TestApi-\${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}"
5691
+ }
5692
+
5693
+ # A REST API needs a resource per path segment, so each operation's path is
5694
+ # split into the distinct path prefixes the resource tree is built from below
5695
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
5696
+
5697
+ path_nodes = merge([
5698
+ for op, segments in local.operation_segments : {
5699
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
5700
+ depth = i
5701
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
5702
+ part = segment
5703
+ }
5704
+ }
5705
+ ]...)
5706
+
5707
+ # Instances of a resource cannot reference one another, so each depth is a
5708
+ # separate resource. Deeper paths are reported by the precondition below.
5709
+ max_path_depth = 16
5710
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
5711
+ for path, node in local.path_nodes : path => node if node.depth == depth
5712
+ } }
5713
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
5714
+
5715
+ resource_ids = merge(
5716
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
5717
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
5718
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
5719
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
5720
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
5721
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
5722
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
5723
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
5724
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
5725
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
5726
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
5727
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
5728
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
5729
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
5730
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
5731
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
5732
+ )
5733
+
5734
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
5735
+ local.resource_ids[join("/", segments)]
5736
+ }
5737
+
5738
+ permission_source_suffix = { for op, details in local.operations : op =>
5739
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
5740
+ }
5741
+ }
5742
+
5743
+ resource "terraform_data" "operations_metadata" {
5744
+ # Fails the plan when the operations metadata has not been generated
5745
+ input = local.operations_file
5746
+
5747
+ lifecycle {
5748
+ precondition {
5749
+ condition = fileexists(local.operations_file)
5750
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
5751
+ }
5752
+ precondition {
5753
+ condition = length(local.operations) > 0
5754
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
5755
+ }
5756
+ precondition {
5757
+ condition = length(local.paths_too_deep) == 0
5758
+ 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."
5759
+ }
5760
+ }
5761
+ }
5762
+
5763
+ # API Gateway resources at path depth 0
5764
+ resource "aws_api_gateway_resource" "path_depth_0" {
5765
+ for_each = local.path_nodes_by_depth[0]
5766
+
5767
+ rest_api_id = module.rest_api.api_id
5768
+ parent_id = module.rest_api.api_root_resource_id
5769
+ path_part = each.value.part
5770
+ }
5771
+
5772
+ # API Gateway resources at path depth 1
5773
+ resource "aws_api_gateway_resource" "path_depth_1" {
5774
+ for_each = local.path_nodes_by_depth[1]
5775
+
5776
+ rest_api_id = module.rest_api.api_id
5777
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
5778
+ path_part = each.value.part
5779
+ }
5780
+
5781
+ # API Gateway resources at path depth 2
5782
+ resource "aws_api_gateway_resource" "path_depth_2" {
5783
+ for_each = local.path_nodes_by_depth[2]
5784
+
5785
+ rest_api_id = module.rest_api.api_id
5786
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
5787
+ path_part = each.value.part
5788
+ }
5789
+
5790
+ # API Gateway resources at path depth 3
5791
+ resource "aws_api_gateway_resource" "path_depth_3" {
5792
+ for_each = local.path_nodes_by_depth[3]
5793
+
5794
+ rest_api_id = module.rest_api.api_id
5795
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
5796
+ path_part = each.value.part
5797
+ }
5798
+
5799
+ # API Gateway resources at path depth 4
5800
+ resource "aws_api_gateway_resource" "path_depth_4" {
5801
+ for_each = local.path_nodes_by_depth[4]
5802
+
5803
+ rest_api_id = module.rest_api.api_id
5804
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
5805
+ path_part = each.value.part
5806
+ }
5807
+
5808
+ # API Gateway resources at path depth 5
5809
+ resource "aws_api_gateway_resource" "path_depth_5" {
5810
+ for_each = local.path_nodes_by_depth[5]
5811
+
5812
+ rest_api_id = module.rest_api.api_id
5813
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
5814
+ path_part = each.value.part
5815
+ }
5816
+
5817
+ # API Gateway resources at path depth 6
5818
+ resource "aws_api_gateway_resource" "path_depth_6" {
5819
+ for_each = local.path_nodes_by_depth[6]
5820
+
5821
+ rest_api_id = module.rest_api.api_id
5822
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
5823
+ path_part = each.value.part
5824
+ }
5825
+
5826
+ # API Gateway resources at path depth 7
5827
+ resource "aws_api_gateway_resource" "path_depth_7" {
5828
+ for_each = local.path_nodes_by_depth[7]
5829
+
5830
+ rest_api_id = module.rest_api.api_id
5831
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
5832
+ path_part = each.value.part
5833
+ }
5834
+
5835
+ # API Gateway resources at path depth 8
5836
+ resource "aws_api_gateway_resource" "path_depth_8" {
5837
+ for_each = local.path_nodes_by_depth[8]
5838
+
5839
+ rest_api_id = module.rest_api.api_id
5840
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
5841
+ path_part = each.value.part
5842
+ }
5843
+
5844
+ # API Gateway resources at path depth 9
5845
+ resource "aws_api_gateway_resource" "path_depth_9" {
5846
+ for_each = local.path_nodes_by_depth[9]
5847
+
5848
+ rest_api_id = module.rest_api.api_id
5849
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
5850
+ path_part = each.value.part
5851
+ }
5852
+
5853
+ # API Gateway resources at path depth 10
5854
+ resource "aws_api_gateway_resource" "path_depth_10" {
5855
+ for_each = local.path_nodes_by_depth[10]
5856
+
5857
+ rest_api_id = module.rest_api.api_id
5858
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
5859
+ path_part = each.value.part
5860
+ }
5861
+
5862
+ # API Gateway resources at path depth 11
5863
+ resource "aws_api_gateway_resource" "path_depth_11" {
5864
+ for_each = local.path_nodes_by_depth[11]
5865
+
5866
+ rest_api_id = module.rest_api.api_id
5867
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
5868
+ path_part = each.value.part
5869
+ }
5870
+
5871
+ # API Gateway resources at path depth 12
5872
+ resource "aws_api_gateway_resource" "path_depth_12" {
5873
+ for_each = local.path_nodes_by_depth[12]
5874
+
5875
+ rest_api_id = module.rest_api.api_id
5876
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
5877
+ path_part = each.value.part
5878
+ }
5879
+
5880
+ # API Gateway resources at path depth 13
5881
+ resource "aws_api_gateway_resource" "path_depth_13" {
5882
+ for_each = local.path_nodes_by_depth[13]
5883
+
5884
+ rest_api_id = module.rest_api.api_id
5885
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
5886
+ path_part = each.value.part
5887
+ }
5888
+
5889
+ # API Gateway resources at path depth 14
5890
+ resource "aws_api_gateway_resource" "path_depth_14" {
5891
+ for_each = local.path_nodes_by_depth[14]
5892
+
5893
+ rest_api_id = module.rest_api.api_id
5894
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
5895
+ path_part = each.value.part
5896
+ }
5897
+
5898
+ # API Gateway resources at path depth 15
5899
+ resource "aws_api_gateway_resource" "path_depth_15" {
5900
+ for_each = local.path_nodes_by_depth[15]
5901
+
5902
+ rest_api_id = module.rest_api.api_id
5903
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
5904
+ path_part = each.value.part
5905
+ }
5906
+
4707
5907
  # Resources
4708
5908
 
4709
5909
  # Create Lambda ZIP file from the bundle directory
@@ -4778,8 +5978,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
4778
5978
  description = "Allow outbound HTTPS to AWS service endpoints"
4779
5979
  }
4780
5980
 
4781
- # Lambda function
4782
5981
  resource "aws_lambda_function" "api_lambda" {
5982
+ # One lambda function per operation, each serving just that operation
5983
+ for_each = local.operations
5984
+
4783
5985
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
4784
5986
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
4785
5987
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -4788,8 +5990,8 @@ resource "aws_lambda_function" "api_lambda" {
4788
5990
  s3_bucket = aws_s3_object.lambda_zip.bucket
4789
5991
  s3_key = aws_s3_object.lambda_zip.key
4790
5992
  s3_object_version = aws_s3_object.lambda_zip.version_id
4791
- function_name = "TestApiHandler-\${random_string.suffix.result}"
4792
- role = aws_iam_role.lambda_execution_role.arn
5993
+ function_name = local.function_name[each.key]
5994
+ role = aws_iam_role.lambda_execution_role[each.key].arn
4793
5995
  handler = "run.sh"
4794
5996
  runtime = "python3.14"
4795
5997
  layers = ["arn:aws:lambda:\${data.aws_region.current.region}:753240598075:layer:LambdaAdapterLayerX86:24"]
@@ -4834,7 +6036,9 @@ resource "aws_lambda_function" "api_lambda" {
4834
6036
 
4835
6037
  # IAM role for Lambda execution
4836
6038
  resource "aws_iam_role" "lambda_execution_role" {
4837
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
6039
+ for_each = local.operations
6040
+
6041
+ name = local.role_name[each.key]
4838
6042
 
4839
6043
  assume_role_policy = jsonencode({
4840
6044
  Version = "2012-10-17"
@@ -4854,32 +6058,38 @@ resource "aws_iam_role" "lambda_execution_role" {
4854
6058
 
4855
6059
  # Attach basic execution policy to Lambda role
4856
6060
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
6061
+ for_each = local.operations
6062
+
4857
6063
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
4858
- role = aws_iam_role.lambda_execution_role.name
6064
+ role = aws_iam_role.lambda_execution_role[each.key].name
4859
6065
  }
4860
6066
 
4861
6067
  # Attach X-Ray tracing policy to Lambda role
4862
6068
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
6069
+ for_each = local.operations
6070
+
4863
6071
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
4864
- role = aws_iam_role.lambda_execution_role.name
6072
+ role = aws_iam_role.lambda_execution_role[each.key].name
4865
6073
  }
4866
6074
 
4867
6075
  # Attach VPC access policy to Lambda role when deployed into a VPC
4868
6076
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
4869
- count = var.enable_vpc ? 1 : 0
6077
+ for_each = var.enable_vpc ? local.operations : {}
6078
+
4870
6079
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
4871
- role = aws_iam_role.lambda_execution_role.name
6080
+ role = aws_iam_role.lambda_execution_role[each.key].name
4872
6081
  }
4873
6082
 
4874
6083
  # Additional IAM policies for Lambda (if provided)
4875
6084
  resource "aws_iam_role_policy" "lambda_additional_policies" {
4876
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
4877
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
4878
- role = aws_iam_role.lambda_execution_role.id
6085
+ for_each = local.operations
6086
+
6087
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
6088
+ role = aws_iam_role.lambda_execution_role[each.key].id
4879
6089
 
4880
6090
  policy = jsonencode({
4881
6091
  Version = "2012-10-17"
4882
- Statement = local.lambda_policy_statements
6092
+ Statement = local.lambda_policy_document_statements
4883
6093
  })
4884
6094
  }
4885
6095
 
@@ -4895,6 +6105,15 @@ locals {
4895
6105
  Resource = ["\${var.appconfig_application_arn}/*"]
4896
6106
  }
4897
6107
  ] : [], var.additional_iam_policy_statements)
6108
+
6109
+ # IAM rejects an empty statement list, so fall back to a no-op deny
6110
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
6111
+ {
6112
+ Effect = "Deny"
6113
+ Action = ["appconfig:GetLatestConfiguration"]
6114
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
6115
+ }
6116
+ ]
4898
6117
  }
4899
6118
 
4900
6119
  # CloudWatch Log Group for Lambda
@@ -4902,15 +6121,19 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
4902
6121
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
4903
6122
  #checkov:skip=CKV_AWS_338:Log retention set to forever
4904
6123
  #checkov:skip=CKV_AWS_66:Log retention set to forever
4905
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
4906
- tags = var.tags
6124
+ for_each = local.operations
6125
+
6126
+ name = "/aws/lambda/\${local.function_name[each.key]}"
6127
+ tags = var.tags
4907
6128
  }
4908
6129
 
4909
6130
  # Lambda alias pointing to the latest published version for SnapStart
4910
6131
  resource "aws_lambda_alias" "live" {
6132
+ for_each = local.operations
6133
+
4911
6134
  name = "live"
4912
- function_name = aws_lambda_function.api_lambda.function_name
4913
- function_version = aws_lambda_function.api_lambda.version
6135
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
6136
+ function_version = aws_lambda_function.api_lambda[each.key].version
4914
6137
 
4915
6138
  depends_on = [aws_lambda_function.api_lambda]
4916
6139
  }
@@ -5008,60 +6231,64 @@ resource "aws_lambda_permission" "authorizer_invoke" {
5008
6231
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
5009
6232
  }
5010
6233
 
5011
- # Create proxy resource (captures all paths)
5012
- resource "aws_api_gateway_resource" "proxy_resource" {
5013
- rest_api_id = module.rest_api.api_id
5014
- parent_id = module.rest_api.api_root_resource_id
5015
- path_part = "{proxy+}"
6234
+ # Method per operation
6235
+ resource "aws_api_gateway_method" "operation_methods" {
6236
+ #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
6237
+ for_each = local.operations
6238
+
6239
+ rest_api_id = module.rest_api.api_id
6240
+ resource_id = local.operation_resource_id[each.key]
6241
+ http_method = upper(each.value.method)
6242
+
6243
+ authorization = "CUSTOM"
6244
+ authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
6245
+
6246
+ # Path parameters must be declared on the method
6247
+ request_parameters = {
6248
+ for segment in local.operation_segments[each.key] :
6249
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
6250
+ if startswith(segment, "{")
6251
+ }
6252
+
6253
+ depends_on = [aws_api_gateway_authorizer.custom_authorizer]
5016
6254
  }
5017
6255
 
5018
- # Lambda integration for REST API
6256
+ # Lambda integration per operation
5019
6257
  resource "aws_api_gateway_integration" "lambda_integration" {
6258
+ for_each = local.operations
6259
+
5020
6260
  rest_api_id = module.rest_api.api_id
5021
- resource_id = aws_api_gateway_resource.proxy_resource.id
5022
- http_method = aws_api_gateway_method.proxy_method.http_method
6261
+ resource_id = local.operation_resource_id[each.key]
6262
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
5023
6263
 
5024
6264
  integration_http_method = "POST"
5025
6265
  type = "AWS_PROXY"
5026
6266
  # Use the response streaming invocation path with the alias ARN for SnapStart
5027
- uri = "arn:aws:apigateway:\${data.aws_region.current.region}:lambda:path/2021-11-15/functions/\${aws_lambda_alias.live.arn}/response-streaming-invocations"
6267
+ 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"
5028
6268
  response_transfer_mode = "STREAM"
5029
6269
 
5030
6270
  depends_on = [aws_lambda_alias.live]
5031
6271
  }
5032
6272
 
5033
- # Method for proxy integration
5034
- resource "aws_api_gateway_method" "proxy_method" {
5035
- #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
5036
- rest_api_id = module.rest_api.api_id
5037
- resource_id = aws_api_gateway_resource.proxy_resource.id
5038
- http_method = "ANY"
5039
-
5040
- authorization = "CUSTOM"
5041
- authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
5042
-
5043
- request_parameters = {
5044
- "method.request.path.proxy" = true
5045
- }
5046
-
5047
- depends_on = [aws_api_gateway_authorizer.custom_authorizer]
5048
- }
5049
-
5050
- # OPTIONS method for CORS preflight
6273
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
5051
6274
  resource "aws_api_gateway_method" "options_method" {
5052
6275
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
5053
6276
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
6277
+ for_each = local.resource_ids
6278
+
5054
6279
  rest_api_id = module.rest_api.api_id
5055
- resource_id = aws_api_gateway_resource.proxy_resource.id
6280
+ resource_id = each.value
5056
6281
  http_method = "OPTIONS"
5057
6282
  authorization = "NONE"
5058
6283
  }
5059
6284
 
5060
- # CORS integration for OPTIONS method
6285
+ # CORS integration for OPTIONS methods
5061
6286
  resource "aws_api_gateway_integration" "options_integration" {
6287
+ for_each = local.resource_ids
6288
+
5062
6289
  rest_api_id = module.rest_api.api_id
5063
- resource_id = aws_api_gateway_resource.proxy_resource.id
5064
- http_method = aws_api_gateway_method.options_method.http_method
6290
+ resource_id = each.value
6291
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
5065
6292
 
5066
6293
  type = "MOCK"
5067
6294
  request_templates = {
@@ -5069,11 +6296,13 @@ resource "aws_api_gateway_integration" "options_integration" {
5069
6296
  }
5070
6297
  }
5071
6298
 
5072
- # OPTIONS method response
6299
+ # OPTIONS method responses
5073
6300
  resource "aws_api_gateway_method_response" "options_response" {
6301
+ for_each = local.resource_ids
6302
+
5074
6303
  rest_api_id = module.rest_api.api_id
5075
- resource_id = aws_api_gateway_resource.proxy_resource.id
5076
- http_method = aws_api_gateway_method.options_method.http_method
6304
+ resource_id = each.value
6305
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
5077
6306
  status_code = "204"
5078
6307
 
5079
6308
  response_parameters = {
@@ -5083,18 +6312,22 @@ resource "aws_api_gateway_method_response" "options_response" {
5083
6312
  }
5084
6313
  }
5085
6314
 
5086
- # OPTIONS integration response
6315
+ # OPTIONS integration responses
5087
6316
  resource "aws_api_gateway_integration_response" "options_integration_response" {
6317
+ for_each = local.resource_ids
6318
+
5088
6319
  rest_api_id = module.rest_api.api_id
5089
- resource_id = aws_api_gateway_resource.proxy_resource.id
5090
- http_method = aws_api_gateway_method.options_method.http_method
5091
- status_code = aws_api_gateway_method_response.options_response.status_code
6320
+ resource_id = each.value
6321
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
6322
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
5092
6323
 
5093
6324
  response_parameters = {
5094
6325
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
5095
6326
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
5096
6327
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
5097
6328
  }
6329
+
6330
+ depends_on = [aws_api_gateway_integration.options_integration]
5098
6331
  }
5099
6332
 
5100
6333
  # API Gateway deployment
@@ -5102,13 +6335,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
5102
6335
  rest_api_id = module.rest_api.api_id
5103
6336
 
5104
6337
  triggers = {
5105
- redeployment = sha1(jsonencode([
5106
- aws_api_gateway_resource.proxy_resource.id,
5107
- aws_api_gateway_method.proxy_method.id,
5108
- aws_api_gateway_integration.lambda_integration.id,
5109
- aws_api_gateway_method.options_method.id,
5110
- aws_api_gateway_integration.options_integration.id,
5111
- ]))
6338
+ redeployment = sha1(jsonencode(concat(
6339
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
6340
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
6341
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
6342
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
6343
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
6344
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
6345
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
6346
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
6347
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
6348
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
6349
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
6350
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
6351
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
6352
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
6353
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
6354
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
6355
+ [for m in aws_api_gateway_method.operation_methods : m.id],
6356
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
6357
+ [for m in aws_api_gateway_method.options_method : m.id],
6358
+ [for i in aws_api_gateway_integration.options_integration : i.id],
6359
+ )))
5112
6360
  }
5113
6361
 
5114
6362
  lifecycle {
@@ -5116,7 +6364,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
5116
6364
  }
5117
6365
 
5118
6366
  depends_on = [
5119
- aws_api_gateway_method.proxy_method,
6367
+ aws_api_gateway_method.operation_methods,
5120
6368
  aws_api_gateway_integration.lambda_integration,
5121
6369
  aws_api_gateway_method.options_method,
5122
6370
  aws_api_gateway_integration.options_integration,
@@ -5245,24 +6493,28 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
5245
6493
 
5246
6494
  # Lambda permission for API Gateway to invoke the function via alias
5247
6495
  resource "aws_lambda_permission" "api_gateway_invoke" {
6496
+ for_each = local.operations
6497
+
5248
6498
  statement_id = "AllowExecutionFromAPIGateway"
5249
6499
  action = "lambda:InvokeFunction"
5250
- function_name = aws_lambda_function.api_lambda.function_name
5251
- qualifier = aws_lambda_alias.live.name
6500
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
6501
+ qualifier = aws_lambda_alias.live[each.key].name
5252
6502
  principal = "apigateway.amazonaws.com"
5253
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
6503
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
5254
6504
 
5255
6505
  depends_on = [module.rest_api, aws_lambda_alias.live]
5256
6506
  }
5257
6507
 
5258
6508
  # Lambda permission for API Gateway to invoke with response streaming via alias
5259
6509
  resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
6510
+ for_each = local.operations
6511
+
5260
6512
  statement_id = "AllowStreamingFromAPIGateway"
5261
6513
  action = "lambda:InvokeFunctionWithResponseStream"
5262
- function_name = aws_lambda_function.api_lambda.function_name
5263
- qualifier = aws_lambda_alias.live.name
6514
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
6515
+ qualifier = aws_lambda_alias.live[each.key].name
5264
6516
  principal = "apigateway.amazonaws.com"
5265
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
6517
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
5266
6518
 
5267
6519
  depends_on = [module.rest_api, aws_lambda_alias.live]
5268
6520
  }
@@ -5273,7 +6525,7 @@ module "add_url_to_runtime_config" {
5273
6525
 
5274
6526
  namespace = "connection"
5275
6527
  key = "apis"
5276
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
6528
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
5277
6529
 
5278
6530
  depends_on = [aws_api_gateway_stage.api_stage]
5279
6531
  }
@@ -5326,83 +6578,83 @@ output "stage_id" {
5326
6578
  value = aws_api_gateway_stage.api_stage.id
5327
6579
  }
5328
6580
 
5329
- # Lambda Function Outputs
5330
- output "lambda_function_name" {
5331
- description = "Name of the Lambda function"
5332
- value = aws_lambda_function.api_lambda.function_name
6581
+ # Lambda Function Outputs, keyed by operation name
6582
+ output "operations" {
6583
+ description = "Names of the API operations, each with its own Lambda function"
6584
+ value = keys(local.operations)
5333
6585
  }
5334
6586
 
5335
- output "lambda_function_arn" {
5336
- description = "ARN of the Lambda function"
5337
- value = aws_lambda_function.api_lambda.arn
6587
+ output "lambda_function_names" {
6588
+ description = "Name of each operation's Lambda function, keyed by operation name"
6589
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
5338
6590
  }
5339
6591
 
5340
- output "lambda_invoke_arn" {
5341
- description = "Invoke ARN of the Lambda function"
5342
- value = aws_lambda_function.api_lambda.invoke_arn
6592
+ output "lambda_function_arns" {
6593
+ description = "ARN of each operation's Lambda function, keyed by operation name"
6594
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
5343
6595
  }
5344
6596
 
5345
- output "lambda_qualified_arn" {
5346
- description = "Qualified ARN of the Lambda function"
5347
- value = aws_lambda_function.api_lambda.qualified_arn
6597
+ output "lambda_invoke_arns" {
6598
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
6599
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
5348
6600
  }
5349
6601
 
5350
- output "lambda_version" {
5351
- description = "Version of the Lambda function"
5352
- value = aws_lambda_function.api_lambda.version
6602
+ output "lambda_qualified_arns" {
6603
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
6604
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
5353
6605
  }
5354
6606
 
5355
- output "lambda_source_code_hash" {
5356
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
5357
- value = aws_lambda_function.api_lambda.source_code_hash
6607
+ output "lambda_versions" {
6608
+ description = "Version of each operation's Lambda function, keyed by operation name"
6609
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
5358
6610
  }
5359
6611
 
5360
- output "lambda_source_code_size" {
5361
- description = "Size of the Lambda deployment package in bytes"
5362
- value = aws_lambda_function.api_lambda.source_code_size
6612
+ output "lambda_source_code_hash" {
6613
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
6614
+ value = data.archive_file.lambda_zip.output_base64sha256
5363
6615
  }
5364
6616
 
5365
- # IAM Role Outputs
5366
- output "lambda_execution_role_arn" {
5367
- description = "ARN of the Lambda execution role"
5368
- value = aws_iam_role.lambda_execution_role.arn
6617
+ # IAM Role Outputs, keyed by operation name
6618
+ output "lambda_execution_role_arns" {
6619
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
6620
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
5369
6621
  }
5370
6622
 
5371
- output "lambda_execution_role_name" {
5372
- description = "Name of the Lambda execution role"
5373
- value = aws_iam_role.lambda_execution_role.name
6623
+ output "lambda_execution_role_names" {
6624
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
6625
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
5374
6626
  }
5375
6627
 
5376
6628
  output "security_group_id" {
5377
- description = "Security group ID of the API Lambda function, for use in ingress rules on resources it must reach (e.g. a database). Null unless enable_vpc is true."
6629
+ 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."
5378
6630
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
5379
6631
  }
5380
6632
 
5381
- # Integration Outputs
5382
- output "integration_id" {
5383
- description = "ID of the Lambda integration"
5384
- value = aws_api_gateway_integration.lambda_integration.id
6633
+ # Integration Outputs, keyed by operation name
6634
+ output "integration_ids" {
6635
+ description = "ID of each operation's Lambda integration, keyed by operation name"
6636
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
5385
6637
  }
5386
6638
 
5387
- output "proxy_resource_id" {
5388
- description = "ID of the proxy resource"
5389
- value = aws_api_gateway_resource.proxy_resource.id
6639
+ output "method_ids" {
6640
+ description = "ID of each operation's API Gateway method, keyed by operation name"
6641
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
5390
6642
  }
5391
6643
 
5392
- output "proxy_method_id" {
5393
- description = "ID of the proxy method"
5394
- value = aws_api_gateway_method.proxy_method.id
6644
+ output "resource_ids" {
6645
+ description = "ID of each API Gateway resource, keyed by its path"
6646
+ value = local.resource_ids
5395
6647
  }
5396
6648
 
5397
- # CloudWatch Log Groups
5398
- output "lambda_log_group_name" {
5399
- description = "Name of the Lambda CloudWatch log group"
5400
- value = aws_cloudwatch_log_group.lambda_logs.name
6649
+ # CloudWatch Log Groups, keyed by operation name
6650
+ output "lambda_log_group_names" {
6651
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
6652
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
5401
6653
  }
5402
6654
 
5403
- output "lambda_log_group_arn" {
5404
- description = "ARN of the Lambda CloudWatch log group"
5405
- value = aws_cloudwatch_log_group.lambda_logs.arn
6655
+ output "lambda_log_group_arns" {
6656
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
6657
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
5406
6658
  }
5407
6659
  ",
5408
6660
  }