@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
@@ -606,22 +606,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
606
606
  }
607
607
  }
608
608
 
609
+ resource "random_id" "waf_log_suffix" {
610
+ count = var.enable_waf ? 1 : 0
611
+ byte_length = 4
612
+ }
613
+
614
+ locals {
615
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
616
+ }
617
+
618
+ # KMS key for encrypting WAF request logs at rest
619
+ resource "aws_kms_key" "waf_logs" {
620
+ count = var.enable_waf ? 1 : 0
621
+
622
+ description = "\${var.api_name} API WAF log encryption"
623
+ deletion_window_in_days = 7
624
+ enable_key_rotation = true
625
+
626
+ policy = jsonencode({
627
+ Version = "2012-10-17"
628
+ Statement = [
629
+ {
630
+ Sid = "Enable IAM User Permissions"
631
+ Effect = "Allow"
632
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
633
+ Action = "kms:*"
634
+ Resource = "*"
635
+ },
636
+ {
637
+ Sid = "Allow CloudWatch Logs"
638
+ Effect = "Allow"
639
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
640
+ Action = [
641
+ "kms:Encrypt",
642
+ "kms:Decrypt",
643
+ "kms:ReEncrypt*",
644
+ "kms:GenerateDataKey*",
645
+ "kms:DescribeKey"
646
+ ]
647
+ Resource = "*"
648
+ Condition = {
649
+ ArnEquals = {
650
+ "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}"
651
+ }
652
+ }
653
+ }
654
+ ]
655
+ })
656
+
657
+ tags = var.tags
658
+ }
659
+
660
+ resource "aws_kms_alias" "waf_logs" {
661
+ count = var.enable_waf ? 1 : 0
662
+
663
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
664
+ target_key_id = aws_kms_key.waf_logs[0].key_id
665
+ }
666
+
609
667
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
610
668
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
611
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
612
669
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
613
670
  count = var.enable_waf ? 1 : 0
614
671
 
615
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
672
+ name = local.waf_log_group_name
616
673
  retention_in_days = 30
674
+ kms_key_id = aws_kms_key.waf_logs[0].arn
617
675
  tags = var.tags
618
676
  }
619
677
 
620
- resource "random_id" "waf_log_suffix" {
621
- count = var.enable_waf ? 1 : 0
622
- byte_length = 4
623
- }
624
-
625
678
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
626
679
  count = var.enable_waf ? 1 : 0
627
680
 
@@ -670,10 +723,18 @@ exports[`tsSmithyApiGenerator > should generate smithy ts api with Terraform pro
670
723
  required_version = ">= 1.0"
671
724
 
672
725
  required_providers {
726
+ archive = {
727
+ source = "hashicorp/archive"
728
+ version = "2.8.0"
729
+ }
673
730
  aws = {
674
731
  source = "hashicorp/aws"
675
732
  version = "6.61.0"
676
733
  }
734
+ random = {
735
+ source = "hashicorp/random"
736
+ version = "3.9.0"
737
+ }
677
738
  }
678
739
  }
679
740
 
@@ -779,6 +840,235 @@ resource "random_string" "suffix" {
779
840
  upper = false
780
841
  }
781
842
 
843
+ locals {
844
+ # Generated at build time from the API definition
845
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
846
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
847
+
848
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
849
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
850
+
851
+ function_name = { for op in keys(local.operations) : op =>
852
+ "\${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}"
853
+ }
854
+ role_name = { for op in keys(local.operations) : op =>
855
+ "\${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}"
856
+ }
857
+
858
+ # A REST API needs a resource per path segment, so each operation's path is
859
+ # split into the distinct path prefixes the resource tree is built from below
860
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
861
+
862
+ path_nodes = merge([
863
+ for op, segments in local.operation_segments : {
864
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
865
+ depth = i
866
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
867
+ part = segment
868
+ }
869
+ }
870
+ ]...)
871
+
872
+ # Instances of a resource cannot reference one another, so each depth is a
873
+ # separate resource. Deeper paths are reported by the precondition below.
874
+ max_path_depth = 16
875
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
876
+ for path, node in local.path_nodes : path => node if node.depth == depth
877
+ } }
878
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
879
+
880
+ resource_ids = merge(
881
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
882
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
883
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
884
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
885
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
886
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
887
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
888
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
889
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
890
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
891
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
892
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
893
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
894
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
895
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
896
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
897
+ )
898
+
899
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
900
+ local.resource_ids[join("/", segments)]
901
+ }
902
+
903
+ permission_source_suffix = { for op, details in local.operations : op =>
904
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
905
+ }
906
+ }
907
+
908
+ resource "terraform_data" "operations_metadata" {
909
+ # Fails the plan when the operations metadata has not been generated
910
+ input = local.operations_file
911
+
912
+ lifecycle {
913
+ precondition {
914
+ condition = fileexists(local.operations_file)
915
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
916
+ }
917
+ precondition {
918
+ condition = length(local.operations) > 0
919
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
920
+ }
921
+ precondition {
922
+ condition = length(local.paths_too_deep) == 0
923
+ 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."
924
+ }
925
+ }
926
+ }
927
+
928
+ # API Gateway resources at path depth 0
929
+ resource "aws_api_gateway_resource" "path_depth_0" {
930
+ for_each = local.path_nodes_by_depth[0]
931
+
932
+ rest_api_id = module.rest_api.api_id
933
+ parent_id = module.rest_api.api_root_resource_id
934
+ path_part = each.value.part
935
+ }
936
+
937
+ # API Gateway resources at path depth 1
938
+ resource "aws_api_gateway_resource" "path_depth_1" {
939
+ for_each = local.path_nodes_by_depth[1]
940
+
941
+ rest_api_id = module.rest_api.api_id
942
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
943
+ path_part = each.value.part
944
+ }
945
+
946
+ # API Gateway resources at path depth 2
947
+ resource "aws_api_gateway_resource" "path_depth_2" {
948
+ for_each = local.path_nodes_by_depth[2]
949
+
950
+ rest_api_id = module.rest_api.api_id
951
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
952
+ path_part = each.value.part
953
+ }
954
+
955
+ # API Gateway resources at path depth 3
956
+ resource "aws_api_gateway_resource" "path_depth_3" {
957
+ for_each = local.path_nodes_by_depth[3]
958
+
959
+ rest_api_id = module.rest_api.api_id
960
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
961
+ path_part = each.value.part
962
+ }
963
+
964
+ # API Gateway resources at path depth 4
965
+ resource "aws_api_gateway_resource" "path_depth_4" {
966
+ for_each = local.path_nodes_by_depth[4]
967
+
968
+ rest_api_id = module.rest_api.api_id
969
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
970
+ path_part = each.value.part
971
+ }
972
+
973
+ # API Gateway resources at path depth 5
974
+ resource "aws_api_gateway_resource" "path_depth_5" {
975
+ for_each = local.path_nodes_by_depth[5]
976
+
977
+ rest_api_id = module.rest_api.api_id
978
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
979
+ path_part = each.value.part
980
+ }
981
+
982
+ # API Gateway resources at path depth 6
983
+ resource "aws_api_gateway_resource" "path_depth_6" {
984
+ for_each = local.path_nodes_by_depth[6]
985
+
986
+ rest_api_id = module.rest_api.api_id
987
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
988
+ path_part = each.value.part
989
+ }
990
+
991
+ # API Gateway resources at path depth 7
992
+ resource "aws_api_gateway_resource" "path_depth_7" {
993
+ for_each = local.path_nodes_by_depth[7]
994
+
995
+ rest_api_id = module.rest_api.api_id
996
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
997
+ path_part = each.value.part
998
+ }
999
+
1000
+ # API Gateway resources at path depth 8
1001
+ resource "aws_api_gateway_resource" "path_depth_8" {
1002
+ for_each = local.path_nodes_by_depth[8]
1003
+
1004
+ rest_api_id = module.rest_api.api_id
1005
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
1006
+ path_part = each.value.part
1007
+ }
1008
+
1009
+ # API Gateway resources at path depth 9
1010
+ resource "aws_api_gateway_resource" "path_depth_9" {
1011
+ for_each = local.path_nodes_by_depth[9]
1012
+
1013
+ rest_api_id = module.rest_api.api_id
1014
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
1015
+ path_part = each.value.part
1016
+ }
1017
+
1018
+ # API Gateway resources at path depth 10
1019
+ resource "aws_api_gateway_resource" "path_depth_10" {
1020
+ for_each = local.path_nodes_by_depth[10]
1021
+
1022
+ rest_api_id = module.rest_api.api_id
1023
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
1024
+ path_part = each.value.part
1025
+ }
1026
+
1027
+ # API Gateway resources at path depth 11
1028
+ resource "aws_api_gateway_resource" "path_depth_11" {
1029
+ for_each = local.path_nodes_by_depth[11]
1030
+
1031
+ rest_api_id = module.rest_api.api_id
1032
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
1033
+ path_part = each.value.part
1034
+ }
1035
+
1036
+ # API Gateway resources at path depth 12
1037
+ resource "aws_api_gateway_resource" "path_depth_12" {
1038
+ for_each = local.path_nodes_by_depth[12]
1039
+
1040
+ rest_api_id = module.rest_api.api_id
1041
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
1042
+ path_part = each.value.part
1043
+ }
1044
+
1045
+ # API Gateway resources at path depth 13
1046
+ resource "aws_api_gateway_resource" "path_depth_13" {
1047
+ for_each = local.path_nodes_by_depth[13]
1048
+
1049
+ rest_api_id = module.rest_api.api_id
1050
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
1051
+ path_part = each.value.part
1052
+ }
1053
+
1054
+ # API Gateway resources at path depth 14
1055
+ resource "aws_api_gateway_resource" "path_depth_14" {
1056
+ for_each = local.path_nodes_by_depth[14]
1057
+
1058
+ rest_api_id = module.rest_api.api_id
1059
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
1060
+ path_part = each.value.part
1061
+ }
1062
+
1063
+ # API Gateway resources at path depth 15
1064
+ resource "aws_api_gateway_resource" "path_depth_15" {
1065
+ for_each = local.path_nodes_by_depth[15]
1066
+
1067
+ rest_api_id = module.rest_api.api_id
1068
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
1069
+ path_part = each.value.part
1070
+ }
1071
+
782
1072
  # Resources
783
1073
 
784
1074
  # Create Lambda ZIP file from the bundle directory
@@ -853,8 +1143,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
853
1143
  description = "Allow outbound HTTPS to AWS service endpoints"
854
1144
  }
855
1145
 
856
- # Lambda function
857
1146
  resource "aws_lambda_function" "api_lambda" {
1147
+ # One lambda function per operation, each serving just that operation
1148
+ for_each = local.operations
1149
+
858
1150
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
859
1151
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
860
1152
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -863,8 +1155,8 @@ resource "aws_lambda_function" "api_lambda" {
863
1155
  s3_bucket = aws_s3_object.lambda_zip.bucket
864
1156
  s3_key = aws_s3_object.lambda_zip.key
865
1157
  s3_object_version = aws_s3_object.lambda_zip.version_id
866
- function_name = "TestApiHandler-\${random_string.suffix.result}"
867
- role = aws_iam_role.lambda_execution_role.arn
1158
+ function_name = local.function_name[each.key]
1159
+ role = aws_iam_role.lambda_execution_role[each.key].arn
868
1160
  handler = "index.handler"
869
1161
  runtime = "nodejs22.x"
870
1162
  timeout = 30
@@ -900,7 +1192,9 @@ resource "aws_lambda_function" "api_lambda" {
900
1192
 
901
1193
  # IAM role for Lambda execution
902
1194
  resource "aws_iam_role" "lambda_execution_role" {
903
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
1195
+ for_each = local.operations
1196
+
1197
+ name = local.role_name[each.key]
904
1198
 
905
1199
  assume_role_policy = jsonencode({
906
1200
  Version = "2012-10-17"
@@ -920,32 +1214,38 @@ resource "aws_iam_role" "lambda_execution_role" {
920
1214
 
921
1215
  # Attach basic execution policy to Lambda role
922
1216
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
1217
+ for_each = local.operations
1218
+
923
1219
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
924
- role = aws_iam_role.lambda_execution_role.name
1220
+ role = aws_iam_role.lambda_execution_role[each.key].name
925
1221
  }
926
1222
 
927
1223
  # Attach X-Ray tracing policy to Lambda role
928
1224
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
1225
+ for_each = local.operations
1226
+
929
1227
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
930
- role = aws_iam_role.lambda_execution_role.name
1228
+ role = aws_iam_role.lambda_execution_role[each.key].name
931
1229
  }
932
1230
 
933
1231
  # Attach VPC access policy to Lambda role when deployed into a VPC
934
1232
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
935
- count = var.enable_vpc ? 1 : 0
1233
+ for_each = var.enable_vpc ? local.operations : {}
1234
+
936
1235
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
937
- role = aws_iam_role.lambda_execution_role.name
1236
+ role = aws_iam_role.lambda_execution_role[each.key].name
938
1237
  }
939
1238
 
940
1239
  # Additional IAM policies for Lambda (if provided)
941
1240
  resource "aws_iam_role_policy" "lambda_additional_policies" {
942
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
943
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
944
- role = aws_iam_role.lambda_execution_role.id
1241
+ for_each = local.operations
1242
+
1243
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
1244
+ role = aws_iam_role.lambda_execution_role[each.key].id
945
1245
 
946
1246
  policy = jsonencode({
947
1247
  Version = "2012-10-17"
948
- Statement = local.lambda_policy_statements
1248
+ Statement = local.lambda_policy_document_statements
949
1249
  })
950
1250
  }
951
1251
 
@@ -961,6 +1261,15 @@ locals {
961
1261
  Resource = ["\${var.appconfig_application_arn}/*"]
962
1262
  }
963
1263
  ] : [], var.additional_iam_policy_statements)
1264
+
1265
+ # IAM rejects an empty statement list, so fall back to a no-op deny
1266
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
1267
+ {
1268
+ Effect = "Deny"
1269
+ Action = ["appconfig:GetLatestConfiguration"]
1270
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
1271
+ }
1272
+ ]
964
1273
  }
965
1274
 
966
1275
  # CloudWatch Log Group for Lambda
@@ -968,8 +1277,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
968
1277
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
969
1278
  #checkov:skip=CKV_AWS_338:Log retention set to forever
970
1279
  #checkov:skip=CKV_AWS_66:Log retention set to forever
971
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
972
- tags = var.tags
1280
+ for_each = local.operations
1281
+
1282
+ name = "/aws/lambda/\${local.function_name[each.key]}"
1283
+ tags = var.tags
973
1284
  }
974
1285
 
975
1286
 
@@ -1066,58 +1377,62 @@ resource "aws_lambda_permission" "authorizer_invoke" {
1066
1377
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
1067
1378
  }
1068
1379
 
1069
- # Create proxy resource (captures all paths)
1070
- resource "aws_api_gateway_resource" "proxy_resource" {
1071
- rest_api_id = module.rest_api.api_id
1072
- parent_id = module.rest_api.api_root_resource_id
1073
- path_part = "{proxy+}"
1074
- }
1075
-
1076
- # Lambda integration for REST API
1077
- resource "aws_api_gateway_integration" "lambda_integration" {
1078
- rest_api_id = module.rest_api.api_id
1079
- resource_id = aws_api_gateway_resource.proxy_resource.id
1080
- http_method = aws_api_gateway_method.proxy_method.http_method
1081
-
1082
- integration_http_method = "POST"
1083
- type = "AWS_PROXY"
1084
- uri = aws_lambda_function.api_lambda.invoke_arn
1085
-
1086
- depends_on = [aws_lambda_function.api_lambda]
1087
- }
1088
-
1089
- # Method for proxy integration
1090
- resource "aws_api_gateway_method" "proxy_method" {
1380
+ # Method per operation
1381
+ resource "aws_api_gateway_method" "operation_methods" {
1091
1382
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
1383
+ for_each = local.operations
1384
+
1092
1385
  rest_api_id = module.rest_api.api_id
1093
- resource_id = aws_api_gateway_resource.proxy_resource.id
1094
- http_method = "ANY"
1386
+ resource_id = local.operation_resource_id[each.key]
1387
+ http_method = upper(each.value.method)
1095
1388
 
1096
1389
  authorization = "CUSTOM"
1097
1390
  authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
1098
1391
 
1392
+ # Path parameters must be declared on the method
1099
1393
  request_parameters = {
1100
- "method.request.path.proxy" = true
1394
+ for segment in local.operation_segments[each.key] :
1395
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
1396
+ if startswith(segment, "{")
1101
1397
  }
1102
1398
 
1103
1399
  depends_on = [aws_api_gateway_authorizer.custom_authorizer]
1104
1400
  }
1105
1401
 
1106
- # OPTIONS method for CORS preflight
1402
+ # Lambda integration per operation
1403
+ resource "aws_api_gateway_integration" "lambda_integration" {
1404
+ for_each = local.operations
1405
+
1406
+ rest_api_id = module.rest_api.api_id
1407
+ resource_id = local.operation_resource_id[each.key]
1408
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
1409
+
1410
+ integration_http_method = "POST"
1411
+ type = "AWS_PROXY"
1412
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
1413
+
1414
+ depends_on = [aws_lambda_function.api_lambda]
1415
+ }
1416
+
1417
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
1107
1418
  resource "aws_api_gateway_method" "options_method" {
1108
1419
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
1109
1420
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
1421
+ for_each = local.resource_ids
1422
+
1110
1423
  rest_api_id = module.rest_api.api_id
1111
- resource_id = aws_api_gateway_resource.proxy_resource.id
1424
+ resource_id = each.value
1112
1425
  http_method = "OPTIONS"
1113
1426
  authorization = "NONE"
1114
1427
  }
1115
1428
 
1116
- # CORS integration for OPTIONS method
1429
+ # CORS integration for OPTIONS methods
1117
1430
  resource "aws_api_gateway_integration" "options_integration" {
1431
+ for_each = local.resource_ids
1432
+
1118
1433
  rest_api_id = module.rest_api.api_id
1119
- resource_id = aws_api_gateway_resource.proxy_resource.id
1120
- http_method = aws_api_gateway_method.options_method.http_method
1434
+ resource_id = each.value
1435
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
1121
1436
 
1122
1437
  type = "MOCK"
1123
1438
  request_templates = {
@@ -1125,11 +1440,13 @@ resource "aws_api_gateway_integration" "options_integration" {
1125
1440
  }
1126
1441
  }
1127
1442
 
1128
- # OPTIONS method response
1443
+ # OPTIONS method responses
1129
1444
  resource "aws_api_gateway_method_response" "options_response" {
1445
+ for_each = local.resource_ids
1446
+
1130
1447
  rest_api_id = module.rest_api.api_id
1131
- resource_id = aws_api_gateway_resource.proxy_resource.id
1132
- http_method = aws_api_gateway_method.options_method.http_method
1448
+ resource_id = each.value
1449
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
1133
1450
  status_code = "204"
1134
1451
 
1135
1452
  response_parameters = {
@@ -1139,18 +1456,22 @@ resource "aws_api_gateway_method_response" "options_response" {
1139
1456
  }
1140
1457
  }
1141
1458
 
1142
- # OPTIONS integration response
1459
+ # OPTIONS integration responses
1143
1460
  resource "aws_api_gateway_integration_response" "options_integration_response" {
1461
+ for_each = local.resource_ids
1462
+
1144
1463
  rest_api_id = module.rest_api.api_id
1145
- resource_id = aws_api_gateway_resource.proxy_resource.id
1146
- http_method = aws_api_gateway_method.options_method.http_method
1147
- status_code = aws_api_gateway_method_response.options_response.status_code
1464
+ resource_id = each.value
1465
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
1466
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
1148
1467
 
1149
1468
  response_parameters = {
1150
1469
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
1151
1470
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
1152
1471
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
1153
1472
  }
1473
+
1474
+ depends_on = [aws_api_gateway_integration.options_integration]
1154
1475
  }
1155
1476
 
1156
1477
  # API Gateway deployment
@@ -1158,13 +1479,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
1158
1479
  rest_api_id = module.rest_api.api_id
1159
1480
 
1160
1481
  triggers = {
1161
- redeployment = sha1(jsonencode([
1162
- aws_api_gateway_resource.proxy_resource.id,
1163
- aws_api_gateway_method.proxy_method.id,
1164
- aws_api_gateway_integration.lambda_integration.id,
1165
- aws_api_gateway_method.options_method.id,
1166
- aws_api_gateway_integration.options_integration.id,
1167
- ]))
1482
+ redeployment = sha1(jsonencode(concat(
1483
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
1484
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
1485
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
1486
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
1487
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
1488
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
1489
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
1490
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
1491
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
1492
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
1493
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
1494
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
1495
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
1496
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
1497
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
1498
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
1499
+ [for m in aws_api_gateway_method.operation_methods : m.id],
1500
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
1501
+ [for m in aws_api_gateway_method.options_method : m.id],
1502
+ [for i in aws_api_gateway_integration.options_integration : i.id],
1503
+ )))
1168
1504
  }
1169
1505
 
1170
1506
  lifecycle {
@@ -1172,7 +1508,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
1172
1508
  }
1173
1509
 
1174
1510
  depends_on = [
1175
- aws_api_gateway_method.proxy_method,
1511
+ aws_api_gateway_method.operation_methods,
1176
1512
  aws_api_gateway_integration.lambda_integration,
1177
1513
  aws_api_gateway_method.options_method,
1178
1514
  aws_api_gateway_integration.options_integration,
@@ -1301,11 +1637,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
1301
1637
 
1302
1638
  # Lambda permission for API Gateway to invoke the function
1303
1639
  resource "aws_lambda_permission" "api_gateway_invoke" {
1640
+ for_each = local.operations
1641
+
1304
1642
  statement_id = "AllowExecutionFromAPIGateway"
1305
1643
  action = "lambda:InvokeFunction"
1306
- function_name = aws_lambda_function.api_lambda.function_name
1644
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1307
1645
  principal = "apigateway.amazonaws.com"
1308
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
1646
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
1309
1647
 
1310
1648
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
1311
1649
  }
@@ -1317,7 +1655,7 @@ module "add_url_to_runtime_config" {
1317
1655
 
1318
1656
  namespace = "connection"
1319
1657
  key = "apis"
1320
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
1658
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
1321
1659
 
1322
1660
  depends_on = [aws_api_gateway_stage.api_stage]
1323
1661
  }
@@ -1370,83 +1708,83 @@ output "stage_id" {
1370
1708
  value = aws_api_gateway_stage.api_stage.id
1371
1709
  }
1372
1710
 
1373
- # Lambda Function Outputs
1374
- output "lambda_function_name" {
1375
- description = "Name of the Lambda function"
1376
- value = aws_lambda_function.api_lambda.function_name
1711
+ # Lambda Function Outputs, keyed by operation name
1712
+ output "operations" {
1713
+ description = "Names of the API operations, each with its own Lambda function"
1714
+ value = keys(local.operations)
1377
1715
  }
1378
1716
 
1379
- output "lambda_function_arn" {
1380
- description = "ARN of the Lambda function"
1381
- value = aws_lambda_function.api_lambda.arn
1717
+ output "lambda_function_names" {
1718
+ description = "Name of each operation's Lambda function, keyed by operation name"
1719
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
1382
1720
  }
1383
1721
 
1384
- output "lambda_invoke_arn" {
1385
- description = "Invoke ARN of the Lambda function"
1386
- value = aws_lambda_function.api_lambda.invoke_arn
1722
+ output "lambda_function_arns" {
1723
+ description = "ARN of each operation's Lambda function, keyed by operation name"
1724
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
1387
1725
  }
1388
1726
 
1389
- output "lambda_qualified_arn" {
1390
- description = "Qualified ARN of the Lambda function"
1391
- value = aws_lambda_function.api_lambda.qualified_arn
1727
+ output "lambda_invoke_arns" {
1728
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
1729
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
1392
1730
  }
1393
1731
 
1394
- output "lambda_version" {
1395
- description = "Version of the Lambda function"
1396
- value = aws_lambda_function.api_lambda.version
1732
+ output "lambda_qualified_arns" {
1733
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
1734
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
1397
1735
  }
1398
1736
 
1399
- output "lambda_source_code_hash" {
1400
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
1401
- value = aws_lambda_function.api_lambda.source_code_hash
1737
+ output "lambda_versions" {
1738
+ description = "Version of each operation's Lambda function, keyed by operation name"
1739
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
1402
1740
  }
1403
1741
 
1404
- output "lambda_source_code_size" {
1405
- description = "Size of the Lambda deployment package in bytes"
1406
- value = aws_lambda_function.api_lambda.source_code_size
1742
+ output "lambda_source_code_hash" {
1743
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
1744
+ value = data.archive_file.lambda_zip.output_base64sha256
1407
1745
  }
1408
1746
 
1409
- # IAM Role Outputs
1410
- output "lambda_execution_role_arn" {
1411
- description = "ARN of the Lambda execution role"
1412
- value = aws_iam_role.lambda_execution_role.arn
1747
+ # IAM Role Outputs, keyed by operation name
1748
+ output "lambda_execution_role_arns" {
1749
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
1750
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
1413
1751
  }
1414
1752
 
1415
- output "lambda_execution_role_name" {
1416
- description = "Name of the Lambda execution role"
1417
- value = aws_iam_role.lambda_execution_role.name
1753
+ output "lambda_execution_role_names" {
1754
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
1755
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
1418
1756
  }
1419
1757
 
1420
1758
  output "security_group_id" {
1421
- 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."
1759
+ 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."
1422
1760
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
1423
1761
  }
1424
1762
 
1425
- # Integration Outputs
1426
- output "integration_id" {
1427
- description = "ID of the Lambda integration"
1428
- value = aws_api_gateway_integration.lambda_integration.id
1763
+ # Integration Outputs, keyed by operation name
1764
+ output "integration_ids" {
1765
+ description = "ID of each operation's Lambda integration, keyed by operation name"
1766
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
1429
1767
  }
1430
1768
 
1431
- output "proxy_resource_id" {
1432
- description = "ID of the proxy resource"
1433
- value = aws_api_gateway_resource.proxy_resource.id
1769
+ output "method_ids" {
1770
+ description = "ID of each operation's API Gateway method, keyed by operation name"
1771
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
1434
1772
  }
1435
1773
 
1436
- output "proxy_method_id" {
1437
- description = "ID of the proxy method"
1438
- value = aws_api_gateway_method.proxy_method.id
1774
+ output "resource_ids" {
1775
+ description = "ID of each API Gateway resource, keyed by its path"
1776
+ value = local.resource_ids
1439
1777
  }
1440
1778
 
1441
- # CloudWatch Log Groups
1442
- output "lambda_log_group_name" {
1443
- description = "Name of the Lambda CloudWatch log group"
1444
- value = aws_cloudwatch_log_group.lambda_logs.name
1779
+ # CloudWatch Log Groups, keyed by operation name
1780
+ output "lambda_log_group_names" {
1781
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
1782
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
1445
1783
  }
1446
1784
 
1447
- output "lambda_log_group_arn" {
1448
- description = "ARN of the Lambda CloudWatch log group"
1449
- value = aws_cloudwatch_log_group.lambda_logs.arn
1785
+ output "lambda_log_group_arns" {
1786
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
1787
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
1450
1788
  }
1451
1789
  "
1452
1790
  `;
@@ -2097,22 +2435,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
2097
2435
  }
2098
2436
  }
2099
2437
 
2438
+ resource "random_id" "waf_log_suffix" {
2439
+ count = var.enable_waf ? 1 : 0
2440
+ byte_length = 4
2441
+ }
2442
+
2443
+ locals {
2444
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
2445
+ }
2446
+
2447
+ # KMS key for encrypting WAF request logs at rest
2448
+ resource "aws_kms_key" "waf_logs" {
2449
+ count = var.enable_waf ? 1 : 0
2450
+
2451
+ description = "\${var.api_name} API WAF log encryption"
2452
+ deletion_window_in_days = 7
2453
+ enable_key_rotation = true
2454
+
2455
+ policy = jsonencode({
2456
+ Version = "2012-10-17"
2457
+ Statement = [
2458
+ {
2459
+ Sid = "Enable IAM User Permissions"
2460
+ Effect = "Allow"
2461
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
2462
+ Action = "kms:*"
2463
+ Resource = "*"
2464
+ },
2465
+ {
2466
+ Sid = "Allow CloudWatch Logs"
2467
+ Effect = "Allow"
2468
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
2469
+ Action = [
2470
+ "kms:Encrypt",
2471
+ "kms:Decrypt",
2472
+ "kms:ReEncrypt*",
2473
+ "kms:GenerateDataKey*",
2474
+ "kms:DescribeKey"
2475
+ ]
2476
+ Resource = "*"
2477
+ Condition = {
2478
+ ArnEquals = {
2479
+ "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}"
2480
+ }
2481
+ }
2482
+ }
2483
+ ]
2484
+ })
2485
+
2486
+ tags = var.tags
2487
+ }
2488
+
2489
+ resource "aws_kms_alias" "waf_logs" {
2490
+ count = var.enable_waf ? 1 : 0
2491
+
2492
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
2493
+ target_key_id = aws_kms_key.waf_logs[0].key_id
2494
+ }
2495
+
2100
2496
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
2101
2497
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
2102
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2103
2498
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
2104
2499
  count = var.enable_waf ? 1 : 0
2105
2500
 
2106
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
2501
+ name = local.waf_log_group_name
2107
2502
  retention_in_days = 30
2503
+ kms_key_id = aws_kms_key.waf_logs[0].arn
2108
2504
  tags = var.tags
2109
2505
  }
2110
2506
 
2111
- resource "random_id" "waf_log_suffix" {
2112
- count = var.enable_waf ? 1 : 0
2113
- byte_length = 4
2114
- }
2115
-
2116
2507
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
2117
2508
  count = var.enable_waf ? 1 : 0
2118
2509
 
@@ -2158,10 +2549,18 @@ output "waf_web_acl_arn" {
2158
2549
  required_version = ">= 1.0"
2159
2550
 
2160
2551
  required_providers {
2552
+ archive = {
2553
+ source = "hashicorp/archive"
2554
+ version = "2.8.0"
2555
+ }
2161
2556
  aws = {
2162
2557
  source = "hashicorp/aws"
2163
2558
  version = "6.61.0"
2164
2559
  }
2560
+ random = {
2561
+ source = "hashicorp/random"
2562
+ version = "3.9.0"
2563
+ }
2165
2564
  }
2166
2565
  }
2167
2566
 
@@ -2277,7 +2676,236 @@ resource "random_string" "suffix" {
2277
2676
  upper = false
2278
2677
  }
2279
2678
 
2280
- # Resources
2679
+ locals {
2680
+ # Generated at build time from the API definition
2681
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
2682
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
2683
+
2684
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
2685
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
2686
+
2687
+ function_name = { for op in keys(local.operations) : op =>
2688
+ "\${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}"
2689
+ }
2690
+ role_name = { for op in keys(local.operations) : op =>
2691
+ "\${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}"
2692
+ }
2693
+
2694
+ # A REST API needs a resource per path segment, so each operation's path is
2695
+ # split into the distinct path prefixes the resource tree is built from below
2696
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
2697
+
2698
+ path_nodes = merge([
2699
+ for op, segments in local.operation_segments : {
2700
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
2701
+ depth = i
2702
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
2703
+ part = segment
2704
+ }
2705
+ }
2706
+ ]...)
2707
+
2708
+ # Instances of a resource cannot reference one another, so each depth is a
2709
+ # separate resource. Deeper paths are reported by the precondition below.
2710
+ max_path_depth = 16
2711
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
2712
+ for path, node in local.path_nodes : path => node if node.depth == depth
2713
+ } }
2714
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
2715
+
2716
+ resource_ids = merge(
2717
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
2718
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
2719
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
2720
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
2721
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
2722
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
2723
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
2724
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
2725
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
2726
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
2727
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
2728
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
2729
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
2730
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
2731
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
2732
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
2733
+ )
2734
+
2735
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
2736
+ local.resource_ids[join("/", segments)]
2737
+ }
2738
+
2739
+ permission_source_suffix = { for op, details in local.operations : op =>
2740
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
2741
+ }
2742
+ }
2743
+
2744
+ resource "terraform_data" "operations_metadata" {
2745
+ # Fails the plan when the operations metadata has not been generated
2746
+ input = local.operations_file
2747
+
2748
+ lifecycle {
2749
+ precondition {
2750
+ condition = fileexists(local.operations_file)
2751
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
2752
+ }
2753
+ precondition {
2754
+ condition = length(local.operations) > 0
2755
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
2756
+ }
2757
+ precondition {
2758
+ condition = length(local.paths_too_deep) == 0
2759
+ 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."
2760
+ }
2761
+ }
2762
+ }
2763
+
2764
+ # API Gateway resources at path depth 0
2765
+ resource "aws_api_gateway_resource" "path_depth_0" {
2766
+ for_each = local.path_nodes_by_depth[0]
2767
+
2768
+ rest_api_id = module.rest_api.api_id
2769
+ parent_id = module.rest_api.api_root_resource_id
2770
+ path_part = each.value.part
2771
+ }
2772
+
2773
+ # API Gateway resources at path depth 1
2774
+ resource "aws_api_gateway_resource" "path_depth_1" {
2775
+ for_each = local.path_nodes_by_depth[1]
2776
+
2777
+ rest_api_id = module.rest_api.api_id
2778
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
2779
+ path_part = each.value.part
2780
+ }
2781
+
2782
+ # API Gateway resources at path depth 2
2783
+ resource "aws_api_gateway_resource" "path_depth_2" {
2784
+ for_each = local.path_nodes_by_depth[2]
2785
+
2786
+ rest_api_id = module.rest_api.api_id
2787
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
2788
+ path_part = each.value.part
2789
+ }
2790
+
2791
+ # API Gateway resources at path depth 3
2792
+ resource "aws_api_gateway_resource" "path_depth_3" {
2793
+ for_each = local.path_nodes_by_depth[3]
2794
+
2795
+ rest_api_id = module.rest_api.api_id
2796
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
2797
+ path_part = each.value.part
2798
+ }
2799
+
2800
+ # API Gateway resources at path depth 4
2801
+ resource "aws_api_gateway_resource" "path_depth_4" {
2802
+ for_each = local.path_nodes_by_depth[4]
2803
+
2804
+ rest_api_id = module.rest_api.api_id
2805
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
2806
+ path_part = each.value.part
2807
+ }
2808
+
2809
+ # API Gateway resources at path depth 5
2810
+ resource "aws_api_gateway_resource" "path_depth_5" {
2811
+ for_each = local.path_nodes_by_depth[5]
2812
+
2813
+ rest_api_id = module.rest_api.api_id
2814
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
2815
+ path_part = each.value.part
2816
+ }
2817
+
2818
+ # API Gateway resources at path depth 6
2819
+ resource "aws_api_gateway_resource" "path_depth_6" {
2820
+ for_each = local.path_nodes_by_depth[6]
2821
+
2822
+ rest_api_id = module.rest_api.api_id
2823
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
2824
+ path_part = each.value.part
2825
+ }
2826
+
2827
+ # API Gateway resources at path depth 7
2828
+ resource "aws_api_gateway_resource" "path_depth_7" {
2829
+ for_each = local.path_nodes_by_depth[7]
2830
+
2831
+ rest_api_id = module.rest_api.api_id
2832
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
2833
+ path_part = each.value.part
2834
+ }
2835
+
2836
+ # API Gateway resources at path depth 8
2837
+ resource "aws_api_gateway_resource" "path_depth_8" {
2838
+ for_each = local.path_nodes_by_depth[8]
2839
+
2840
+ rest_api_id = module.rest_api.api_id
2841
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
2842
+ path_part = each.value.part
2843
+ }
2844
+
2845
+ # API Gateway resources at path depth 9
2846
+ resource "aws_api_gateway_resource" "path_depth_9" {
2847
+ for_each = local.path_nodes_by_depth[9]
2848
+
2849
+ rest_api_id = module.rest_api.api_id
2850
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
2851
+ path_part = each.value.part
2852
+ }
2853
+
2854
+ # API Gateway resources at path depth 10
2855
+ resource "aws_api_gateway_resource" "path_depth_10" {
2856
+ for_each = local.path_nodes_by_depth[10]
2857
+
2858
+ rest_api_id = module.rest_api.api_id
2859
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
2860
+ path_part = each.value.part
2861
+ }
2862
+
2863
+ # API Gateway resources at path depth 11
2864
+ resource "aws_api_gateway_resource" "path_depth_11" {
2865
+ for_each = local.path_nodes_by_depth[11]
2866
+
2867
+ rest_api_id = module.rest_api.api_id
2868
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
2869
+ path_part = each.value.part
2870
+ }
2871
+
2872
+ # API Gateway resources at path depth 12
2873
+ resource "aws_api_gateway_resource" "path_depth_12" {
2874
+ for_each = local.path_nodes_by_depth[12]
2875
+
2876
+ rest_api_id = module.rest_api.api_id
2877
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
2878
+ path_part = each.value.part
2879
+ }
2880
+
2881
+ # API Gateway resources at path depth 13
2882
+ resource "aws_api_gateway_resource" "path_depth_13" {
2883
+ for_each = local.path_nodes_by_depth[13]
2884
+
2885
+ rest_api_id = module.rest_api.api_id
2886
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
2887
+ path_part = each.value.part
2888
+ }
2889
+
2890
+ # API Gateway resources at path depth 14
2891
+ resource "aws_api_gateway_resource" "path_depth_14" {
2892
+ for_each = local.path_nodes_by_depth[14]
2893
+
2894
+ rest_api_id = module.rest_api.api_id
2895
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
2896
+ path_part = each.value.part
2897
+ }
2898
+
2899
+ # API Gateway resources at path depth 15
2900
+ resource "aws_api_gateway_resource" "path_depth_15" {
2901
+ for_each = local.path_nodes_by_depth[15]
2902
+
2903
+ rest_api_id = module.rest_api.api_id
2904
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
2905
+ path_part = each.value.part
2906
+ }
2907
+
2908
+ # Resources
2281
2909
 
2282
2910
  # Create Lambda ZIP file from the bundle directory
2283
2911
  data "archive_file" "lambda_zip" {
@@ -2351,8 +2979,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
2351
2979
  description = "Allow outbound HTTPS to AWS service endpoints"
2352
2980
  }
2353
2981
 
2354
- # Lambda function
2355
2982
  resource "aws_lambda_function" "api_lambda" {
2983
+ # One lambda function per operation, each serving just that operation
2984
+ for_each = local.operations
2985
+
2356
2986
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
2357
2987
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
2358
2988
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -2361,8 +2991,8 @@ resource "aws_lambda_function" "api_lambda" {
2361
2991
  s3_bucket = aws_s3_object.lambda_zip.bucket
2362
2992
  s3_key = aws_s3_object.lambda_zip.key
2363
2993
  s3_object_version = aws_s3_object.lambda_zip.version_id
2364
- function_name = "TestApiHandler-\${random_string.suffix.result}"
2365
- role = aws_iam_role.lambda_execution_role.arn
2994
+ function_name = local.function_name[each.key]
2995
+ role = aws_iam_role.lambda_execution_role[each.key].arn
2366
2996
  handler = "index.handler"
2367
2997
  runtime = "nodejs22.x"
2368
2998
  timeout = 30
@@ -2398,7 +3028,9 @@ resource "aws_lambda_function" "api_lambda" {
2398
3028
 
2399
3029
  # IAM role for Lambda execution
2400
3030
  resource "aws_iam_role" "lambda_execution_role" {
2401
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
3031
+ for_each = local.operations
3032
+
3033
+ name = local.role_name[each.key]
2402
3034
 
2403
3035
  assume_role_policy = jsonencode({
2404
3036
  Version = "2012-10-17"
@@ -2418,32 +3050,38 @@ resource "aws_iam_role" "lambda_execution_role" {
2418
3050
 
2419
3051
  # Attach basic execution policy to Lambda role
2420
3052
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
3053
+ for_each = local.operations
3054
+
2421
3055
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
2422
- role = aws_iam_role.lambda_execution_role.name
3056
+ role = aws_iam_role.lambda_execution_role[each.key].name
2423
3057
  }
2424
3058
 
2425
3059
  # Attach X-Ray tracing policy to Lambda role
2426
3060
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
3061
+ for_each = local.operations
3062
+
2427
3063
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
2428
- role = aws_iam_role.lambda_execution_role.name
3064
+ role = aws_iam_role.lambda_execution_role[each.key].name
2429
3065
  }
2430
3066
 
2431
3067
  # Attach VPC access policy to Lambda role when deployed into a VPC
2432
3068
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
2433
- count = var.enable_vpc ? 1 : 0
3069
+ for_each = var.enable_vpc ? local.operations : {}
3070
+
2434
3071
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
2435
- role = aws_iam_role.lambda_execution_role.name
3072
+ role = aws_iam_role.lambda_execution_role[each.key].name
2436
3073
  }
2437
3074
 
2438
3075
  # Additional IAM policies for Lambda (if provided)
2439
3076
  resource "aws_iam_role_policy" "lambda_additional_policies" {
2440
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
2441
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
2442
- role = aws_iam_role.lambda_execution_role.id
3077
+ for_each = local.operations
3078
+
3079
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
3080
+ role = aws_iam_role.lambda_execution_role[each.key].id
2443
3081
 
2444
3082
  policy = jsonencode({
2445
3083
  Version = "2012-10-17"
2446
- Statement = local.lambda_policy_statements
3084
+ Statement = local.lambda_policy_document_statements
2447
3085
  })
2448
3086
  }
2449
3087
 
@@ -2459,6 +3097,15 @@ locals {
2459
3097
  Resource = ["\${var.appconfig_application_arn}/*"]
2460
3098
  }
2461
3099
  ] : [], var.additional_iam_policy_statements)
3100
+
3101
+ # IAM rejects an empty statement list, so fall back to a no-op deny
3102
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
3103
+ {
3104
+ Effect = "Deny"
3105
+ Action = ["appconfig:GetLatestConfiguration"]
3106
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
3107
+ }
3108
+ ]
2462
3109
  }
2463
3110
 
2464
3111
  # CloudWatch Log Group for Lambda
@@ -2466,8 +3113,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
2466
3113
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2467
3114
  #checkov:skip=CKV_AWS_338:Log retention set to forever
2468
3115
  #checkov:skip=CKV_AWS_66:Log retention set to forever
2469
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
2470
- tags = var.tags
3116
+ for_each = local.operations
3117
+
3118
+ name = "/aws/lambda/\${local.function_name[each.key]}"
3119
+ tags = var.tags
2471
3120
  }
2472
3121
 
2473
3122
 
@@ -2480,32 +3129,14 @@ resource "aws_api_gateway_authorizer" "cognito_authorizer" {
2480
3129
  identity_source = "method.request.header.Authorization"
2481
3130
  }
2482
3131
 
2483
- # Create proxy resource (captures all paths)
2484
- resource "aws_api_gateway_resource" "proxy_resource" {
2485
- rest_api_id = module.rest_api.api_id
2486
- parent_id = module.rest_api.api_root_resource_id
2487
- path_part = "{proxy+}"
2488
- }
2489
-
2490
- # Lambda integration for REST API
2491
- resource "aws_api_gateway_integration" "lambda_integration" {
2492
- rest_api_id = module.rest_api.api_id
2493
- resource_id = aws_api_gateway_resource.proxy_resource.id
2494
- http_method = aws_api_gateway_method.proxy_method.http_method
2495
-
2496
- integration_http_method = "POST"
2497
- type = "AWS_PROXY"
2498
- uri = aws_lambda_function.api_lambda.invoke_arn
2499
-
2500
- depends_on = [aws_lambda_function.api_lambda]
2501
- }
2502
-
2503
- # Method for proxy integration
2504
- resource "aws_api_gateway_method" "proxy_method" {
3132
+ # Method per operation
3133
+ resource "aws_api_gateway_method" "operation_methods" {
2505
3134
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
3135
+ for_each = local.operations
3136
+
2506
3137
  rest_api_id = module.rest_api.api_id
2507
- resource_id = aws_api_gateway_resource.proxy_resource.id
2508
- http_method = "ANY"
3138
+ resource_id = local.operation_resource_id[each.key]
3139
+ http_method = upper(each.value.method)
2509
3140
 
2510
3141
  authorization = "COGNITO_USER_POOLS"
2511
3142
  authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
@@ -2513,28 +3144,50 @@ resource "aws_api_gateway_method" "proxy_method" {
2513
3144
  # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
2514
3145
  authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
2515
3146
 
3147
+ # Path parameters must be declared on the method
2516
3148
  request_parameters = {
2517
- "method.request.path.proxy" = true
3149
+ for segment in local.operation_segments[each.key] :
3150
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
3151
+ if startswith(segment, "{")
2518
3152
  }
2519
3153
 
2520
3154
  depends_on = [aws_api_gateway_authorizer.cognito_authorizer]
2521
3155
  }
2522
3156
 
2523
- # OPTIONS method for CORS preflight
3157
+ # Lambda integration per operation
3158
+ resource "aws_api_gateway_integration" "lambda_integration" {
3159
+ for_each = local.operations
3160
+
3161
+ rest_api_id = module.rest_api.api_id
3162
+ resource_id = local.operation_resource_id[each.key]
3163
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
3164
+
3165
+ integration_http_method = "POST"
3166
+ type = "AWS_PROXY"
3167
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
3168
+
3169
+ depends_on = [aws_lambda_function.api_lambda]
3170
+ }
3171
+
3172
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
2524
3173
  resource "aws_api_gateway_method" "options_method" {
2525
3174
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
2526
3175
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
3176
+ for_each = local.resource_ids
3177
+
2527
3178
  rest_api_id = module.rest_api.api_id
2528
- resource_id = aws_api_gateway_resource.proxy_resource.id
3179
+ resource_id = each.value
2529
3180
  http_method = "OPTIONS"
2530
3181
  authorization = "NONE"
2531
3182
  }
2532
3183
 
2533
- # CORS integration for OPTIONS method
3184
+ # CORS integration for OPTIONS methods
2534
3185
  resource "aws_api_gateway_integration" "options_integration" {
3186
+ for_each = local.resource_ids
3187
+
2535
3188
  rest_api_id = module.rest_api.api_id
2536
- resource_id = aws_api_gateway_resource.proxy_resource.id
2537
- http_method = aws_api_gateway_method.options_method.http_method
3189
+ resource_id = each.value
3190
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
2538
3191
 
2539
3192
  type = "MOCK"
2540
3193
  request_templates = {
@@ -2542,11 +3195,13 @@ resource "aws_api_gateway_integration" "options_integration" {
2542
3195
  }
2543
3196
  }
2544
3197
 
2545
- # OPTIONS method response
3198
+ # OPTIONS method responses
2546
3199
  resource "aws_api_gateway_method_response" "options_response" {
3200
+ for_each = local.resource_ids
3201
+
2547
3202
  rest_api_id = module.rest_api.api_id
2548
- resource_id = aws_api_gateway_resource.proxy_resource.id
2549
- http_method = aws_api_gateway_method.options_method.http_method
3203
+ resource_id = each.value
3204
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
2550
3205
  status_code = "204"
2551
3206
 
2552
3207
  response_parameters = {
@@ -2556,18 +3211,22 @@ resource "aws_api_gateway_method_response" "options_response" {
2556
3211
  }
2557
3212
  }
2558
3213
 
2559
- # OPTIONS integration response
3214
+ # OPTIONS integration responses
2560
3215
  resource "aws_api_gateway_integration_response" "options_integration_response" {
3216
+ for_each = local.resource_ids
3217
+
2561
3218
  rest_api_id = module.rest_api.api_id
2562
- resource_id = aws_api_gateway_resource.proxy_resource.id
2563
- http_method = aws_api_gateway_method.options_method.http_method
2564
- status_code = aws_api_gateway_method_response.options_response.status_code
3219
+ resource_id = each.value
3220
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3221
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
2565
3222
 
2566
3223
  response_parameters = {
2567
3224
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
2568
3225
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
2569
3226
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
2570
3227
  }
3228
+
3229
+ depends_on = [aws_api_gateway_integration.options_integration]
2571
3230
  }
2572
3231
 
2573
3232
  # API Gateway deployment
@@ -2575,13 +3234,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
2575
3234
  rest_api_id = module.rest_api.api_id
2576
3235
 
2577
3236
  triggers = {
2578
- redeployment = sha1(jsonencode([
2579
- aws_api_gateway_resource.proxy_resource.id,
2580
- aws_api_gateway_method.proxy_method.id,
2581
- aws_api_gateway_integration.lambda_integration.id,
2582
- aws_api_gateway_method.options_method.id,
2583
- aws_api_gateway_integration.options_integration.id,
2584
- ]))
3237
+ redeployment = sha1(jsonencode(concat(
3238
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
3239
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
3240
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
3241
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
3242
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
3243
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
3244
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
3245
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
3246
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
3247
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
3248
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
3249
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
3250
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
3251
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
3252
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
3253
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
3254
+ [for m in aws_api_gateway_method.operation_methods : m.id],
3255
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
3256
+ [for m in aws_api_gateway_method.options_method : m.id],
3257
+ [for i in aws_api_gateway_integration.options_integration : i.id],
3258
+ )))
2585
3259
  }
2586
3260
 
2587
3261
  lifecycle {
@@ -2589,7 +3263,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
2589
3263
  }
2590
3264
 
2591
3265
  depends_on = [
2592
- aws_api_gateway_method.proxy_method,
3266
+ aws_api_gateway_method.operation_methods,
2593
3267
  aws_api_gateway_integration.lambda_integration,
2594
3268
  aws_api_gateway_method.options_method,
2595
3269
  aws_api_gateway_integration.options_integration,
@@ -2717,11 +3391,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
2717
3391
 
2718
3392
  # Lambda permission for API Gateway to invoke the function
2719
3393
  resource "aws_lambda_permission" "api_gateway_invoke" {
3394
+ for_each = local.operations
3395
+
2720
3396
  statement_id = "AllowExecutionFromAPIGateway"
2721
3397
  action = "lambda:InvokeFunction"
2722
- function_name = aws_lambda_function.api_lambda.function_name
3398
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
2723
3399
  principal = "apigateway.amazonaws.com"
2724
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
3400
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
2725
3401
 
2726
3402
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
2727
3403
  }
@@ -2733,7 +3409,7 @@ module "add_url_to_runtime_config" {
2733
3409
 
2734
3410
  namespace = "connection"
2735
3411
  key = "apis"
2736
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
3412
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
2737
3413
 
2738
3414
  depends_on = [aws_api_gateway_stage.api_stage]
2739
3415
  }
@@ -2786,83 +3462,83 @@ output "stage_id" {
2786
3462
  value = aws_api_gateway_stage.api_stage.id
2787
3463
  }
2788
3464
 
2789
- # Lambda Function Outputs
2790
- output "lambda_function_name" {
2791
- description = "Name of the Lambda function"
2792
- value = aws_lambda_function.api_lambda.function_name
3465
+ # Lambda Function Outputs, keyed by operation name
3466
+ output "operations" {
3467
+ description = "Names of the API operations, each with its own Lambda function"
3468
+ value = keys(local.operations)
2793
3469
  }
2794
3470
 
2795
- output "lambda_function_arn" {
2796
- description = "ARN of the Lambda function"
2797
- value = aws_lambda_function.api_lambda.arn
3471
+ output "lambda_function_names" {
3472
+ description = "Name of each operation's Lambda function, keyed by operation name"
3473
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
2798
3474
  }
2799
3475
 
2800
- output "lambda_invoke_arn" {
2801
- description = "Invoke ARN of the Lambda function"
2802
- value = aws_lambda_function.api_lambda.invoke_arn
3476
+ output "lambda_function_arns" {
3477
+ description = "ARN of each operation's Lambda function, keyed by operation name"
3478
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
2803
3479
  }
2804
3480
 
2805
- output "lambda_qualified_arn" {
2806
- description = "Qualified ARN of the Lambda function"
2807
- value = aws_lambda_function.api_lambda.qualified_arn
3481
+ output "lambda_invoke_arns" {
3482
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
3483
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
2808
3484
  }
2809
3485
 
2810
- output "lambda_version" {
2811
- description = "Version of the Lambda function"
2812
- value = aws_lambda_function.api_lambda.version
3486
+ output "lambda_qualified_arns" {
3487
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
3488
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
2813
3489
  }
2814
3490
 
2815
- output "lambda_source_code_hash" {
2816
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
2817
- value = aws_lambda_function.api_lambda.source_code_hash
3491
+ output "lambda_versions" {
3492
+ description = "Version of each operation's Lambda function, keyed by operation name"
3493
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
2818
3494
  }
2819
3495
 
2820
- output "lambda_source_code_size" {
2821
- description = "Size of the Lambda deployment package in bytes"
2822
- value = aws_lambda_function.api_lambda.source_code_size
3496
+ output "lambda_source_code_hash" {
3497
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
3498
+ value = data.archive_file.lambda_zip.output_base64sha256
2823
3499
  }
2824
3500
 
2825
- # IAM Role Outputs
2826
- output "lambda_execution_role_arn" {
2827
- description = "ARN of the Lambda execution role"
2828
- value = aws_iam_role.lambda_execution_role.arn
3501
+ # IAM Role Outputs, keyed by operation name
3502
+ output "lambda_execution_role_arns" {
3503
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
3504
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
2829
3505
  }
2830
3506
 
2831
- output "lambda_execution_role_name" {
2832
- description = "Name of the Lambda execution role"
2833
- value = aws_iam_role.lambda_execution_role.name
3507
+ output "lambda_execution_role_names" {
3508
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
3509
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
2834
3510
  }
2835
3511
 
2836
3512
  output "security_group_id" {
2837
- 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."
3513
+ 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."
2838
3514
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
2839
3515
  }
2840
3516
 
2841
- # Integration Outputs
2842
- output "integration_id" {
2843
- description = "ID of the Lambda integration"
2844
- value = aws_api_gateway_integration.lambda_integration.id
3517
+ # Integration Outputs, keyed by operation name
3518
+ output "integration_ids" {
3519
+ description = "ID of each operation's Lambda integration, keyed by operation name"
3520
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
2845
3521
  }
2846
3522
 
2847
- output "proxy_resource_id" {
2848
- description = "ID of the proxy resource"
2849
- value = aws_api_gateway_resource.proxy_resource.id
3523
+ output "method_ids" {
3524
+ description = "ID of each operation's API Gateway method, keyed by operation name"
3525
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
2850
3526
  }
2851
3527
 
2852
- output "proxy_method_id" {
2853
- description = "ID of the proxy method"
2854
- value = aws_api_gateway_method.proxy_method.id
3528
+ output "resource_ids" {
3529
+ description = "ID of each API Gateway resource, keyed by its path"
3530
+ value = local.resource_ids
2855
3531
  }
2856
3532
 
2857
- # CloudWatch Log Groups
2858
- output "lambda_log_group_name" {
2859
- description = "Name of the Lambda CloudWatch log group"
2860
- value = aws_cloudwatch_log_group.lambda_logs.name
3533
+ # CloudWatch Log Groups, keyed by operation name
3534
+ output "lambda_log_group_names" {
3535
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
3536
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
2861
3537
  }
2862
3538
 
2863
- output "lambda_log_group_arn" {
2864
- description = "ARN of the Lambda CloudWatch log group"
2865
- value = aws_cloudwatch_log_group.lambda_logs.arn
3539
+ output "lambda_log_group_arns" {
3540
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
3541
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
2866
3542
  }
2867
3543
  ",
2868
3544
  }
@@ -3072,22 +3748,75 @@ resource "aws_wafv2_web_acl" "api_waf" {
3072
3748
  }
3073
3749
  }
3074
3750
 
3751
+ resource "random_id" "waf_log_suffix" {
3752
+ count = var.enable_waf ? 1 : 0
3753
+ byte_length = 4
3754
+ }
3755
+
3756
+ locals {
3757
+ waf_log_group_name = var.enable_waf ? "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}" : null
3758
+ }
3759
+
3760
+ # KMS key for encrypting WAF request logs at rest
3761
+ resource "aws_kms_key" "waf_logs" {
3762
+ count = var.enable_waf ? 1 : 0
3763
+
3764
+ description = "\${var.api_name} API WAF log encryption"
3765
+ deletion_window_in_days = 7
3766
+ enable_key_rotation = true
3767
+
3768
+ policy = jsonencode({
3769
+ Version = "2012-10-17"
3770
+ Statement = [
3771
+ {
3772
+ Sid = "Enable IAM User Permissions"
3773
+ Effect = "Allow"
3774
+ Principal = { AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root" }
3775
+ Action = "kms:*"
3776
+ Resource = "*"
3777
+ },
3778
+ {
3779
+ Sid = "Allow CloudWatch Logs"
3780
+ Effect = "Allow"
3781
+ Principal = { Service = "logs.\${data.aws_region.current.region}.amazonaws.com" }
3782
+ Action = [
3783
+ "kms:Encrypt",
3784
+ "kms:Decrypt",
3785
+ "kms:ReEncrypt*",
3786
+ "kms:GenerateDataKey*",
3787
+ "kms:DescribeKey"
3788
+ ]
3789
+ Resource = "*"
3790
+ Condition = {
3791
+ ArnEquals = {
3792
+ "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}"
3793
+ }
3794
+ }
3795
+ }
3796
+ ]
3797
+ })
3798
+
3799
+ tags = var.tags
3800
+ }
3801
+
3802
+ resource "aws_kms_alias" "waf_logs" {
3803
+ count = var.enable_waf ? 1 : 0
3804
+
3805
+ name = "alias/\${var.api_name}-\${random_id.waf_log_suffix[0].hex}-waf-logs"
3806
+ target_key_id = aws_kms_key.waf_logs[0].key_id
3807
+ }
3808
+
3075
3809
  # CloudWatch Log Group for WAF request logs. Name must start with \`aws-waf-logs-\`.
3076
3810
  resource "aws_cloudwatch_log_group" "api_waf_logs" {
3077
- #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3078
3811
  #checkov:skip=CKV_AWS_338:Log retention set to one month which is sufficient for WAF logs
3079
3812
  count = var.enable_waf ? 1 : 0
3080
3813
 
3081
- name = "aws-waf-logs-\${var.api_name}-\${random_id.waf_log_suffix[0].hex}"
3814
+ name = local.waf_log_group_name
3082
3815
  retention_in_days = 30
3816
+ kms_key_id = aws_kms_key.waf_logs[0].arn
3083
3817
  tags = var.tags
3084
3818
  }
3085
3819
 
3086
- resource "random_id" "waf_log_suffix" {
3087
- count = var.enable_waf ? 1 : 0
3088
- byte_length = 4
3089
- }
3090
-
3091
3820
  resource "aws_wafv2_web_acl_logging_configuration" "api_waf_logging" {
3092
3821
  count = var.enable_waf ? 1 : 0
3093
3822
 
@@ -3133,10 +3862,18 @@ output "waf_web_acl_arn" {
3133
3862
  required_version = ">= 1.0"
3134
3863
 
3135
3864
  required_providers {
3865
+ archive = {
3866
+ source = "hashicorp/archive"
3867
+ version = "2.8.0"
3868
+ }
3136
3869
  aws = {
3137
3870
  source = "hashicorp/aws"
3138
3871
  version = "6.61.0"
3139
3872
  }
3873
+ random = {
3874
+ source = "hashicorp/random"
3875
+ version = "3.9.0"
3876
+ }
3140
3877
  }
3141
3878
  }
3142
3879
 
@@ -3242,6 +3979,235 @@ resource "random_string" "suffix" {
3242
3979
  upper = false
3243
3980
  }
3244
3981
 
3982
+ locals {
3983
+ # Generated at build time from the API definition
3984
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
3985
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
3986
+
3987
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
3988
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
3989
+
3990
+ function_name = { for op in keys(local.operations) : op =>
3991
+ "\${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}"
3992
+ }
3993
+ role_name = { for op in keys(local.operations) : op =>
3994
+ "\${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}"
3995
+ }
3996
+
3997
+ # A REST API needs a resource per path segment, so each operation's path is
3998
+ # split into the distinct path prefixes the resource tree is built from below
3999
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
4000
+
4001
+ path_nodes = merge([
4002
+ for op, segments in local.operation_segments : {
4003
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
4004
+ depth = i
4005
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
4006
+ part = segment
4007
+ }
4008
+ }
4009
+ ]...)
4010
+
4011
+ # Instances of a resource cannot reference one another, so each depth is a
4012
+ # separate resource. Deeper paths are reported by the precondition below.
4013
+ max_path_depth = 16
4014
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
4015
+ for path, node in local.path_nodes : path => node if node.depth == depth
4016
+ } }
4017
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
4018
+
4019
+ resource_ids = merge(
4020
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
4021
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
4022
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
4023
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
4024
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
4025
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
4026
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
4027
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
4028
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
4029
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
4030
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
4031
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
4032
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
4033
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
4034
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
4035
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
4036
+ )
4037
+
4038
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
4039
+ local.resource_ids[join("/", segments)]
4040
+ }
4041
+
4042
+ permission_source_suffix = { for op, details in local.operations : op =>
4043
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
4044
+ }
4045
+ }
4046
+
4047
+ resource "terraform_data" "operations_metadata" {
4048
+ # Fails the plan when the operations metadata has not been generated
4049
+ input = local.operations_file
4050
+
4051
+ lifecycle {
4052
+ precondition {
4053
+ condition = fileexists(local.operations_file)
4054
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
4055
+ }
4056
+ precondition {
4057
+ condition = length(local.operations) > 0
4058
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
4059
+ }
4060
+ precondition {
4061
+ condition = length(local.paths_too_deep) == 0
4062
+ 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."
4063
+ }
4064
+ }
4065
+ }
4066
+
4067
+ # API Gateway resources at path depth 0
4068
+ resource "aws_api_gateway_resource" "path_depth_0" {
4069
+ for_each = local.path_nodes_by_depth[0]
4070
+
4071
+ rest_api_id = module.rest_api.api_id
4072
+ parent_id = module.rest_api.api_root_resource_id
4073
+ path_part = each.value.part
4074
+ }
4075
+
4076
+ # API Gateway resources at path depth 1
4077
+ resource "aws_api_gateway_resource" "path_depth_1" {
4078
+ for_each = local.path_nodes_by_depth[1]
4079
+
4080
+ rest_api_id = module.rest_api.api_id
4081
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
4082
+ path_part = each.value.part
4083
+ }
4084
+
4085
+ # API Gateway resources at path depth 2
4086
+ resource "aws_api_gateway_resource" "path_depth_2" {
4087
+ for_each = local.path_nodes_by_depth[2]
4088
+
4089
+ rest_api_id = module.rest_api.api_id
4090
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
4091
+ path_part = each.value.part
4092
+ }
4093
+
4094
+ # API Gateway resources at path depth 3
4095
+ resource "aws_api_gateway_resource" "path_depth_3" {
4096
+ for_each = local.path_nodes_by_depth[3]
4097
+
4098
+ rest_api_id = module.rest_api.api_id
4099
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
4100
+ path_part = each.value.part
4101
+ }
4102
+
4103
+ # API Gateway resources at path depth 4
4104
+ resource "aws_api_gateway_resource" "path_depth_4" {
4105
+ for_each = local.path_nodes_by_depth[4]
4106
+
4107
+ rest_api_id = module.rest_api.api_id
4108
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
4109
+ path_part = each.value.part
4110
+ }
4111
+
4112
+ # API Gateway resources at path depth 5
4113
+ resource "aws_api_gateway_resource" "path_depth_5" {
4114
+ for_each = local.path_nodes_by_depth[5]
4115
+
4116
+ rest_api_id = module.rest_api.api_id
4117
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
4118
+ path_part = each.value.part
4119
+ }
4120
+
4121
+ # API Gateway resources at path depth 6
4122
+ resource "aws_api_gateway_resource" "path_depth_6" {
4123
+ for_each = local.path_nodes_by_depth[6]
4124
+
4125
+ rest_api_id = module.rest_api.api_id
4126
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
4127
+ path_part = each.value.part
4128
+ }
4129
+
4130
+ # API Gateway resources at path depth 7
4131
+ resource "aws_api_gateway_resource" "path_depth_7" {
4132
+ for_each = local.path_nodes_by_depth[7]
4133
+
4134
+ rest_api_id = module.rest_api.api_id
4135
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
4136
+ path_part = each.value.part
4137
+ }
4138
+
4139
+ # API Gateway resources at path depth 8
4140
+ resource "aws_api_gateway_resource" "path_depth_8" {
4141
+ for_each = local.path_nodes_by_depth[8]
4142
+
4143
+ rest_api_id = module.rest_api.api_id
4144
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
4145
+ path_part = each.value.part
4146
+ }
4147
+
4148
+ # API Gateway resources at path depth 9
4149
+ resource "aws_api_gateway_resource" "path_depth_9" {
4150
+ for_each = local.path_nodes_by_depth[9]
4151
+
4152
+ rest_api_id = module.rest_api.api_id
4153
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
4154
+ path_part = each.value.part
4155
+ }
4156
+
4157
+ # API Gateway resources at path depth 10
4158
+ resource "aws_api_gateway_resource" "path_depth_10" {
4159
+ for_each = local.path_nodes_by_depth[10]
4160
+
4161
+ rest_api_id = module.rest_api.api_id
4162
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
4163
+ path_part = each.value.part
4164
+ }
4165
+
4166
+ # API Gateway resources at path depth 11
4167
+ resource "aws_api_gateway_resource" "path_depth_11" {
4168
+ for_each = local.path_nodes_by_depth[11]
4169
+
4170
+ rest_api_id = module.rest_api.api_id
4171
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
4172
+ path_part = each.value.part
4173
+ }
4174
+
4175
+ # API Gateway resources at path depth 12
4176
+ resource "aws_api_gateway_resource" "path_depth_12" {
4177
+ for_each = local.path_nodes_by_depth[12]
4178
+
4179
+ rest_api_id = module.rest_api.api_id
4180
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
4181
+ path_part = each.value.part
4182
+ }
4183
+
4184
+ # API Gateway resources at path depth 13
4185
+ resource "aws_api_gateway_resource" "path_depth_13" {
4186
+ for_each = local.path_nodes_by_depth[13]
4187
+
4188
+ rest_api_id = module.rest_api.api_id
4189
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
4190
+ path_part = each.value.part
4191
+ }
4192
+
4193
+ # API Gateway resources at path depth 14
4194
+ resource "aws_api_gateway_resource" "path_depth_14" {
4195
+ for_each = local.path_nodes_by_depth[14]
4196
+
4197
+ rest_api_id = module.rest_api.api_id
4198
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
4199
+ path_part = each.value.part
4200
+ }
4201
+
4202
+ # API Gateway resources at path depth 15
4203
+ resource "aws_api_gateway_resource" "path_depth_15" {
4204
+ for_each = local.path_nodes_by_depth[15]
4205
+
4206
+ rest_api_id = module.rest_api.api_id
4207
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
4208
+ path_part = each.value.part
4209
+ }
4210
+
3245
4211
  # Resources
3246
4212
 
3247
4213
  # Create Lambda ZIP file from the bundle directory
@@ -3316,8 +4282,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
3316
4282
  description = "Allow outbound HTTPS to AWS service endpoints"
3317
4283
  }
3318
4284
 
3319
- # Lambda function
3320
4285
  resource "aws_lambda_function" "api_lambda" {
4286
+ # One lambda function per operation, each serving just that operation
4287
+ for_each = local.operations
4288
+
3321
4289
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
3322
4290
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
3323
4291
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -3326,8 +4294,8 @@ resource "aws_lambda_function" "api_lambda" {
3326
4294
  s3_bucket = aws_s3_object.lambda_zip.bucket
3327
4295
  s3_key = aws_s3_object.lambda_zip.key
3328
4296
  s3_object_version = aws_s3_object.lambda_zip.version_id
3329
- function_name = "TestApiHandler-\${random_string.suffix.result}"
3330
- role = aws_iam_role.lambda_execution_role.arn
4297
+ function_name = local.function_name[each.key]
4298
+ role = aws_iam_role.lambda_execution_role[each.key].arn
3331
4299
  handler = "index.handler"
3332
4300
  runtime = "nodejs22.x"
3333
4301
  timeout = 30
@@ -3363,7 +4331,9 @@ resource "aws_lambda_function" "api_lambda" {
3363
4331
 
3364
4332
  # IAM role for Lambda execution
3365
4333
  resource "aws_iam_role" "lambda_execution_role" {
3366
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
4334
+ for_each = local.operations
4335
+
4336
+ name = local.role_name[each.key]
3367
4337
 
3368
4338
  assume_role_policy = jsonencode({
3369
4339
  Version = "2012-10-17"
@@ -3383,32 +4353,38 @@ resource "aws_iam_role" "lambda_execution_role" {
3383
4353
 
3384
4354
  # Attach basic execution policy to Lambda role
3385
4355
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
4356
+ for_each = local.operations
4357
+
3386
4358
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
3387
- role = aws_iam_role.lambda_execution_role.name
4359
+ role = aws_iam_role.lambda_execution_role[each.key].name
3388
4360
  }
3389
4361
 
3390
4362
  # Attach X-Ray tracing policy to Lambda role
3391
4363
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
4364
+ for_each = local.operations
4365
+
3392
4366
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
3393
- role = aws_iam_role.lambda_execution_role.name
4367
+ role = aws_iam_role.lambda_execution_role[each.key].name
3394
4368
  }
3395
4369
 
3396
4370
  # Attach VPC access policy to Lambda role when deployed into a VPC
3397
4371
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
3398
- count = var.enable_vpc ? 1 : 0
4372
+ for_each = var.enable_vpc ? local.operations : {}
4373
+
3399
4374
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
3400
- role = aws_iam_role.lambda_execution_role.name
4375
+ role = aws_iam_role.lambda_execution_role[each.key].name
3401
4376
  }
3402
4377
 
3403
4378
  # Additional IAM policies for Lambda (if provided)
3404
4379
  resource "aws_iam_role_policy" "lambda_additional_policies" {
3405
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
3406
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
3407
- role = aws_iam_role.lambda_execution_role.id
4380
+ for_each = local.operations
4381
+
4382
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
4383
+ role = aws_iam_role.lambda_execution_role[each.key].id
3408
4384
 
3409
4385
  policy = jsonencode({
3410
4386
  Version = "2012-10-17"
3411
- Statement = local.lambda_policy_statements
4387
+ Statement = local.lambda_policy_document_statements
3412
4388
  })
3413
4389
  }
3414
4390
 
@@ -3424,6 +4400,15 @@ locals {
3424
4400
  Resource = ["\${var.appconfig_application_arn}/*"]
3425
4401
  }
3426
4402
  ] : [], var.additional_iam_policy_statements)
4403
+
4404
+ # IAM rejects an empty statement list, so fall back to a no-op deny
4405
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
4406
+ {
4407
+ Effect = "Deny"
4408
+ Action = ["appconfig:GetLatestConfiguration"]
4409
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
4410
+ }
4411
+ ]
3427
4412
  }
3428
4413
 
3429
4414
  # CloudWatch Log Group for Lambda
@@ -3431,63 +4416,69 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
3431
4416
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3432
4417
  #checkov:skip=CKV_AWS_338:Log retention set to forever
3433
4418
  #checkov:skip=CKV_AWS_66:Log retention set to forever
3434
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
3435
- tags = var.tags
3436
- }
4419
+ for_each = local.operations
3437
4420
 
3438
-
3439
-
3440
- # Create proxy resource (captures all paths)
3441
- resource "aws_api_gateway_resource" "proxy_resource" {
3442
- rest_api_id = module.rest_api.api_id
3443
- parent_id = module.rest_api.api_root_resource_id
3444
- path_part = "{proxy+}"
4421
+ name = "/aws/lambda/\${local.function_name[each.key]}"
4422
+ tags = var.tags
3445
4423
  }
3446
4424
 
3447
- # Lambda integration for REST API
3448
- resource "aws_api_gateway_integration" "lambda_integration" {
3449
- rest_api_id = module.rest_api.api_id
3450
- resource_id = aws_api_gateway_resource.proxy_resource.id
3451
- http_method = aws_api_gateway_method.proxy_method.http_method
3452
4425
 
3453
- integration_http_method = "POST"
3454
- type = "AWS_PROXY"
3455
- uri = aws_lambda_function.api_lambda.invoke_arn
3456
4426
 
3457
- depends_on = [aws_lambda_function.api_lambda]
3458
- }
3459
-
3460
- # Method for proxy integration
3461
- resource "aws_api_gateway_method" "proxy_method" {
4427
+ # Method per operation
4428
+ resource "aws_api_gateway_method" "operation_methods" {
3462
4429
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
4430
+ for_each = local.operations
4431
+
3463
4432
  rest_api_id = module.rest_api.api_id
3464
- resource_id = aws_api_gateway_resource.proxy_resource.id
3465
- http_method = "ANY"
4433
+ resource_id = local.operation_resource_id[each.key]
4434
+ http_method = upper(each.value.method)
3466
4435
 
3467
4436
  authorization = "AWS_IAM"
3468
4437
 
4438
+ # Path parameters must be declared on the method
3469
4439
  request_parameters = {
3470
- "method.request.path.proxy" = true
4440
+ for segment in local.operation_segments[each.key] :
4441
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
4442
+ if startswith(segment, "{")
3471
4443
  }
3472
4444
 
3473
4445
  depends_on = []
3474
4446
  }
3475
4447
 
3476
- # OPTIONS method for CORS preflight
4448
+ # Lambda integration per operation
4449
+ resource "aws_api_gateway_integration" "lambda_integration" {
4450
+ for_each = local.operations
4451
+
4452
+ rest_api_id = module.rest_api.api_id
4453
+ resource_id = local.operation_resource_id[each.key]
4454
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
4455
+
4456
+ integration_http_method = "POST"
4457
+ type = "AWS_PROXY"
4458
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
4459
+
4460
+ depends_on = [aws_lambda_function.api_lambda]
4461
+ }
4462
+
4463
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
3477
4464
  resource "aws_api_gateway_method" "options_method" {
3478
4465
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
3479
4466
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
4467
+ for_each = local.resource_ids
4468
+
3480
4469
  rest_api_id = module.rest_api.api_id
3481
- resource_id = aws_api_gateway_resource.proxy_resource.id
4470
+ resource_id = each.value
3482
4471
  http_method = "OPTIONS"
3483
4472
  authorization = "NONE"
3484
4473
  }
3485
4474
 
3486
- # CORS integration for OPTIONS method
4475
+ # CORS integration for OPTIONS methods
3487
4476
  resource "aws_api_gateway_integration" "options_integration" {
4477
+ for_each = local.resource_ids
4478
+
3488
4479
  rest_api_id = module.rest_api.api_id
3489
- resource_id = aws_api_gateway_resource.proxy_resource.id
3490
- http_method = aws_api_gateway_method.options_method.http_method
4480
+ resource_id = each.value
4481
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3491
4482
 
3492
4483
  type = "MOCK"
3493
4484
  request_templates = {
@@ -3495,11 +4486,13 @@ resource "aws_api_gateway_integration" "options_integration" {
3495
4486
  }
3496
4487
  }
3497
4488
 
3498
- # OPTIONS method response
4489
+ # OPTIONS method responses
3499
4490
  resource "aws_api_gateway_method_response" "options_response" {
4491
+ for_each = local.resource_ids
4492
+
3500
4493
  rest_api_id = module.rest_api.api_id
3501
- resource_id = aws_api_gateway_resource.proxy_resource.id
3502
- http_method = aws_api_gateway_method.options_method.http_method
4494
+ resource_id = each.value
4495
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3503
4496
  status_code = "204"
3504
4497
 
3505
4498
  response_parameters = {
@@ -3509,18 +4502,22 @@ resource "aws_api_gateway_method_response" "options_response" {
3509
4502
  }
3510
4503
  }
3511
4504
 
3512
- # OPTIONS integration response
4505
+ # OPTIONS integration responses
3513
4506
  resource "aws_api_gateway_integration_response" "options_integration_response" {
4507
+ for_each = local.resource_ids
4508
+
3514
4509
  rest_api_id = module.rest_api.api_id
3515
- resource_id = aws_api_gateway_resource.proxy_resource.id
3516
- http_method = aws_api_gateway_method.options_method.http_method
3517
- status_code = aws_api_gateway_method_response.options_response.status_code
4510
+ resource_id = each.value
4511
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
4512
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
3518
4513
 
3519
4514
  response_parameters = {
3520
4515
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
3521
4516
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
3522
4517
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
3523
4518
  }
4519
+
4520
+ depends_on = [aws_api_gateway_integration.options_integration]
3524
4521
  }
3525
4522
 
3526
4523
  # API Gateway deployment
@@ -3528,13 +4525,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3528
4525
  rest_api_id = module.rest_api.api_id
3529
4526
 
3530
4527
  triggers = {
3531
- redeployment = sha1(jsonencode([
3532
- aws_api_gateway_resource.proxy_resource.id,
3533
- aws_api_gateway_method.proxy_method.id,
3534
- aws_api_gateway_integration.lambda_integration.id,
3535
- aws_api_gateway_method.options_method.id,
3536
- aws_api_gateway_integration.options_integration.id,
3537
- ]))
4528
+ redeployment = sha1(jsonencode(concat(
4529
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
4530
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
4531
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
4532
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
4533
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
4534
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
4535
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
4536
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
4537
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
4538
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
4539
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
4540
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
4541
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
4542
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
4543
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
4544
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
4545
+ [for m in aws_api_gateway_method.operation_methods : m.id],
4546
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
4547
+ [for m in aws_api_gateway_method.options_method : m.id],
4548
+ [for i in aws_api_gateway_integration.options_integration : i.id],
4549
+ )))
3538
4550
  }
3539
4551
 
3540
4552
  lifecycle {
@@ -3542,7 +4554,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3542
4554
  }
3543
4555
 
3544
4556
  depends_on = [
3545
- aws_api_gateway_method.proxy_method,
4557
+ aws_api_gateway_method.operation_methods,
3546
4558
  aws_api_gateway_integration.lambda_integration,
3547
4559
  aws_api_gateway_method.options_method,
3548
4560
  aws_api_gateway_integration.options_integration,
@@ -3681,11 +4693,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
3681
4693
 
3682
4694
  # Lambda permission for API Gateway to invoke the function
3683
4695
  resource "aws_lambda_permission" "api_gateway_invoke" {
4696
+ for_each = local.operations
4697
+
3684
4698
  statement_id = "AllowExecutionFromAPIGateway"
3685
4699
  action = "lambda:InvokeFunction"
3686
- function_name = aws_lambda_function.api_lambda.function_name
4700
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3687
4701
  principal = "apigateway.amazonaws.com"
3688
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
4702
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
3689
4703
 
3690
4704
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
3691
4705
  }
@@ -3697,7 +4711,7 @@ module "add_url_to_runtime_config" {
3697
4711
 
3698
4712
  namespace = "connection"
3699
4713
  key = "apis"
3700
- value = { "TestApi" = aws_api_gateway_stage.api_stage.invoke_url }
4714
+ value = { "TestApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
3701
4715
 
3702
4716
  depends_on = [aws_api_gateway_stage.api_stage]
3703
4717
  }
@@ -3750,83 +4764,83 @@ output "stage_id" {
3750
4764
  value = aws_api_gateway_stage.api_stage.id
3751
4765
  }
3752
4766
 
3753
- # Lambda Function Outputs
3754
- output "lambda_function_name" {
3755
- description = "Name of the Lambda function"
3756
- value = aws_lambda_function.api_lambda.function_name
4767
+ # Lambda Function Outputs, keyed by operation name
4768
+ output "operations" {
4769
+ description = "Names of the API operations, each with its own Lambda function"
4770
+ value = keys(local.operations)
3757
4771
  }
3758
4772
 
3759
- output "lambda_function_arn" {
3760
- description = "ARN of the Lambda function"
3761
- value = aws_lambda_function.api_lambda.arn
4773
+ output "lambda_function_names" {
4774
+ description = "Name of each operation's Lambda function, keyed by operation name"
4775
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
3762
4776
  }
3763
4777
 
3764
- output "lambda_invoke_arn" {
3765
- description = "Invoke ARN of the Lambda function"
3766
- value = aws_lambda_function.api_lambda.invoke_arn
4778
+ output "lambda_function_arns" {
4779
+ description = "ARN of each operation's Lambda function, keyed by operation name"
4780
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
3767
4781
  }
3768
4782
 
3769
- output "lambda_qualified_arn" {
3770
- description = "Qualified ARN of the Lambda function"
3771
- value = aws_lambda_function.api_lambda.qualified_arn
4783
+ output "lambda_invoke_arns" {
4784
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
4785
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
3772
4786
  }
3773
4787
 
3774
- output "lambda_version" {
3775
- description = "Version of the Lambda function"
3776
- value = aws_lambda_function.api_lambda.version
4788
+ output "lambda_qualified_arns" {
4789
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
4790
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
3777
4791
  }
3778
4792
 
3779
- output "lambda_source_code_hash" {
3780
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
3781
- value = aws_lambda_function.api_lambda.source_code_hash
4793
+ output "lambda_versions" {
4794
+ description = "Version of each operation's Lambda function, keyed by operation name"
4795
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
3782
4796
  }
3783
4797
 
3784
- output "lambda_source_code_size" {
3785
- description = "Size of the Lambda deployment package in bytes"
3786
- value = aws_lambda_function.api_lambda.source_code_size
4798
+ output "lambda_source_code_hash" {
4799
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
4800
+ value = data.archive_file.lambda_zip.output_base64sha256
3787
4801
  }
3788
4802
 
3789
- # IAM Role Outputs
3790
- output "lambda_execution_role_arn" {
3791
- description = "ARN of the Lambda execution role"
3792
- value = aws_iam_role.lambda_execution_role.arn
4803
+ # IAM Role Outputs, keyed by operation name
4804
+ output "lambda_execution_role_arns" {
4805
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
4806
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
3793
4807
  }
3794
4808
 
3795
- output "lambda_execution_role_name" {
3796
- description = "Name of the Lambda execution role"
3797
- value = aws_iam_role.lambda_execution_role.name
4809
+ output "lambda_execution_role_names" {
4810
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
4811
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
3798
4812
  }
3799
4813
 
3800
4814
  output "security_group_id" {
3801
- 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."
4815
+ 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."
3802
4816
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
3803
4817
  }
3804
4818
 
3805
- # Integration Outputs
3806
- output "integration_id" {
3807
- description = "ID of the Lambda integration"
3808
- value = aws_api_gateway_integration.lambda_integration.id
4819
+ # Integration Outputs, keyed by operation name
4820
+ output "integration_ids" {
4821
+ description = "ID of each operation's Lambda integration, keyed by operation name"
4822
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
3809
4823
  }
3810
4824
 
3811
- output "proxy_resource_id" {
3812
- description = "ID of the proxy resource"
3813
- value = aws_api_gateway_resource.proxy_resource.id
4825
+ output "method_ids" {
4826
+ description = "ID of each operation's API Gateway method, keyed by operation name"
4827
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
3814
4828
  }
3815
4829
 
3816
- output "proxy_method_id" {
3817
- description = "ID of the proxy method"
3818
- value = aws_api_gateway_method.proxy_method.id
4830
+ output "resource_ids" {
4831
+ description = "ID of each API Gateway resource, keyed by its path"
4832
+ value = local.resource_ids
3819
4833
  }
3820
4834
 
3821
- # CloudWatch Log Groups
3822
- output "lambda_log_group_name" {
3823
- description = "Name of the Lambda CloudWatch log group"
3824
- value = aws_cloudwatch_log_group.lambda_logs.name
4835
+ # CloudWatch Log Groups, keyed by operation name
4836
+ output "lambda_log_group_names" {
4837
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
4838
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
3825
4839
  }
3826
4840
 
3827
- output "lambda_log_group_arn" {
3828
- description = "ARN of the Lambda CloudWatch log group"
3829
- value = aws_cloudwatch_log_group.lambda_logs.arn
4841
+ output "lambda_log_group_arns" {
4842
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
4843
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
3830
4844
  }
3831
4845
  ",
3832
4846
  }
@@ -3837,10 +4851,18 @@ exports[`tsSmithyApiGenerator > terraform iac > should generate terraform with c
3837
4851
  required_version = ">= 1.0"
3838
4852
 
3839
4853
  required_providers {
4854
+ archive = {
4855
+ source = "hashicorp/archive"
4856
+ version = "2.8.0"
4857
+ }
3840
4858
  aws = {
3841
4859
  source = "hashicorp/aws"
3842
4860
  version = "6.61.0"
3843
4861
  }
4862
+ random = {
4863
+ source = "hashicorp/random"
4864
+ version = "3.9.0"
4865
+ }
3844
4866
  }
3845
4867
  }
3846
4868
 
@@ -3946,6 +4968,235 @@ resource "random_string" "suffix" {
3946
4968
  upper = false
3947
4969
  }
3948
4970
 
4971
+ locals {
4972
+ # Generated at build time from the API definition
4973
+ operations_file = "\${path.module}/../../../generated/custom-api/operations.json"
4974
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
4975
+
4976
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
4977
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
4978
+
4979
+ function_name = { for op in keys(local.operations) : op =>
4980
+ "\${substr("CustomApi-\${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}"
4981
+ }
4982
+ role_name = { for op in keys(local.operations) : op =>
4983
+ "\${substr("CustomApi-\${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}"
4984
+ }
4985
+
4986
+ # A REST API needs a resource per path segment, so each operation's path is
4987
+ # split into the distinct path prefixes the resource tree is built from below
4988
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
4989
+
4990
+ path_nodes = merge([
4991
+ for op, segments in local.operation_segments : {
4992
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
4993
+ depth = i
4994
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
4995
+ part = segment
4996
+ }
4997
+ }
4998
+ ]...)
4999
+
5000
+ # Instances of a resource cannot reference one another, so each depth is a
5001
+ # separate resource. Deeper paths are reported by the precondition below.
5002
+ max_path_depth = 16
5003
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
5004
+ for path, node in local.path_nodes : path => node if node.depth == depth
5005
+ } }
5006
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
5007
+
5008
+ resource_ids = merge(
5009
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
5010
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
5011
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
5012
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
5013
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
5014
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
5015
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
5016
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
5017
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
5018
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
5019
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
5020
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
5021
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
5022
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
5023
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
5024
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
5025
+ )
5026
+
5027
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
5028
+ local.resource_ids[join("/", segments)]
5029
+ }
5030
+
5031
+ permission_source_suffix = { for op, details in local.operations : op =>
5032
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
5033
+ }
5034
+ }
5035
+
5036
+ resource "terraform_data" "operations_metadata" {
5037
+ # Fails the plan when the operations metadata has not been generated
5038
+ input = local.operations_file
5039
+
5040
+ lifecycle {
5041
+ precondition {
5042
+ condition = fileexists(local.operations_file)
5043
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the CustomApi project to generate it."
5044
+ }
5045
+ precondition {
5046
+ condition = length(local.operations) > 0
5047
+ error_message = "No operations found in \${local.operations_file}. The CustomApi API must define at least one operation."
5048
+ }
5049
+ precondition {
5050
+ condition = length(local.paths_too_deep) == 0
5051
+ 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."
5052
+ }
5053
+ }
5054
+ }
5055
+
5056
+ # API Gateway resources at path depth 0
5057
+ resource "aws_api_gateway_resource" "path_depth_0" {
5058
+ for_each = local.path_nodes_by_depth[0]
5059
+
5060
+ rest_api_id = module.rest_api.api_id
5061
+ parent_id = module.rest_api.api_root_resource_id
5062
+ path_part = each.value.part
5063
+ }
5064
+
5065
+ # API Gateway resources at path depth 1
5066
+ resource "aws_api_gateway_resource" "path_depth_1" {
5067
+ for_each = local.path_nodes_by_depth[1]
5068
+
5069
+ rest_api_id = module.rest_api.api_id
5070
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
5071
+ path_part = each.value.part
5072
+ }
5073
+
5074
+ # API Gateway resources at path depth 2
5075
+ resource "aws_api_gateway_resource" "path_depth_2" {
5076
+ for_each = local.path_nodes_by_depth[2]
5077
+
5078
+ rest_api_id = module.rest_api.api_id
5079
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
5080
+ path_part = each.value.part
5081
+ }
5082
+
5083
+ # API Gateway resources at path depth 3
5084
+ resource "aws_api_gateway_resource" "path_depth_3" {
5085
+ for_each = local.path_nodes_by_depth[3]
5086
+
5087
+ rest_api_id = module.rest_api.api_id
5088
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
5089
+ path_part = each.value.part
5090
+ }
5091
+
5092
+ # API Gateway resources at path depth 4
5093
+ resource "aws_api_gateway_resource" "path_depth_4" {
5094
+ for_each = local.path_nodes_by_depth[4]
5095
+
5096
+ rest_api_id = module.rest_api.api_id
5097
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
5098
+ path_part = each.value.part
5099
+ }
5100
+
5101
+ # API Gateway resources at path depth 5
5102
+ resource "aws_api_gateway_resource" "path_depth_5" {
5103
+ for_each = local.path_nodes_by_depth[5]
5104
+
5105
+ rest_api_id = module.rest_api.api_id
5106
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
5107
+ path_part = each.value.part
5108
+ }
5109
+
5110
+ # API Gateway resources at path depth 6
5111
+ resource "aws_api_gateway_resource" "path_depth_6" {
5112
+ for_each = local.path_nodes_by_depth[6]
5113
+
5114
+ rest_api_id = module.rest_api.api_id
5115
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
5116
+ path_part = each.value.part
5117
+ }
5118
+
5119
+ # API Gateway resources at path depth 7
5120
+ resource "aws_api_gateway_resource" "path_depth_7" {
5121
+ for_each = local.path_nodes_by_depth[7]
5122
+
5123
+ rest_api_id = module.rest_api.api_id
5124
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
5125
+ path_part = each.value.part
5126
+ }
5127
+
5128
+ # API Gateway resources at path depth 8
5129
+ resource "aws_api_gateway_resource" "path_depth_8" {
5130
+ for_each = local.path_nodes_by_depth[8]
5131
+
5132
+ rest_api_id = module.rest_api.api_id
5133
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
5134
+ path_part = each.value.part
5135
+ }
5136
+
5137
+ # API Gateway resources at path depth 9
5138
+ resource "aws_api_gateway_resource" "path_depth_9" {
5139
+ for_each = local.path_nodes_by_depth[9]
5140
+
5141
+ rest_api_id = module.rest_api.api_id
5142
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
5143
+ path_part = each.value.part
5144
+ }
5145
+
5146
+ # API Gateway resources at path depth 10
5147
+ resource "aws_api_gateway_resource" "path_depth_10" {
5148
+ for_each = local.path_nodes_by_depth[10]
5149
+
5150
+ rest_api_id = module.rest_api.api_id
5151
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
5152
+ path_part = each.value.part
5153
+ }
5154
+
5155
+ # API Gateway resources at path depth 11
5156
+ resource "aws_api_gateway_resource" "path_depth_11" {
5157
+ for_each = local.path_nodes_by_depth[11]
5158
+
5159
+ rest_api_id = module.rest_api.api_id
5160
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
5161
+ path_part = each.value.part
5162
+ }
5163
+
5164
+ # API Gateway resources at path depth 12
5165
+ resource "aws_api_gateway_resource" "path_depth_12" {
5166
+ for_each = local.path_nodes_by_depth[12]
5167
+
5168
+ rest_api_id = module.rest_api.api_id
5169
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
5170
+ path_part = each.value.part
5171
+ }
5172
+
5173
+ # API Gateway resources at path depth 13
5174
+ resource "aws_api_gateway_resource" "path_depth_13" {
5175
+ for_each = local.path_nodes_by_depth[13]
5176
+
5177
+ rest_api_id = module.rest_api.api_id
5178
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
5179
+ path_part = each.value.part
5180
+ }
5181
+
5182
+ # API Gateway resources at path depth 14
5183
+ resource "aws_api_gateway_resource" "path_depth_14" {
5184
+ for_each = local.path_nodes_by_depth[14]
5185
+
5186
+ rest_api_id = module.rest_api.api_id
5187
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
5188
+ path_part = each.value.part
5189
+ }
5190
+
5191
+ # API Gateway resources at path depth 15
5192
+ resource "aws_api_gateway_resource" "path_depth_15" {
5193
+ for_each = local.path_nodes_by_depth[15]
5194
+
5195
+ rest_api_id = module.rest_api.api_id
5196
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
5197
+ path_part = each.value.part
5198
+ }
5199
+
3949
5200
  # Resources
3950
5201
 
3951
5202
  # Create Lambda ZIP file from the bundle directory
@@ -4020,8 +5271,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
4020
5271
  description = "Allow outbound HTTPS to AWS service endpoints"
4021
5272
  }
4022
5273
 
4023
- # Lambda function
4024
5274
  resource "aws_lambda_function" "api_lambda" {
5275
+ # One lambda function per operation, each serving just that operation
5276
+ for_each = local.operations
5277
+
4025
5278
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
4026
5279
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
4027
5280
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -4030,8 +5283,8 @@ resource "aws_lambda_function" "api_lambda" {
4030
5283
  s3_bucket = aws_s3_object.lambda_zip.bucket
4031
5284
  s3_key = aws_s3_object.lambda_zip.key
4032
5285
  s3_object_version = aws_s3_object.lambda_zip.version_id
4033
- function_name = "CustomApiHandler-\${random_string.suffix.result}"
4034
- role = aws_iam_role.lambda_execution_role.arn
5286
+ function_name = local.function_name[each.key]
5287
+ role = aws_iam_role.lambda_execution_role[each.key].arn
4035
5288
  handler = "index.handler"
4036
5289
  runtime = "nodejs22.x"
4037
5290
  timeout = 30
@@ -4067,7 +5320,9 @@ resource "aws_lambda_function" "api_lambda" {
4067
5320
 
4068
5321
  # IAM role for Lambda execution
4069
5322
  resource "aws_iam_role" "lambda_execution_role" {
4070
- name = "CustomApiHandler-execution-role-\${random_string.suffix.result}"
5323
+ for_each = local.operations
5324
+
5325
+ name = local.role_name[each.key]
4071
5326
 
4072
5327
  assume_role_policy = jsonencode({
4073
5328
  Version = "2012-10-17"
@@ -4087,32 +5342,38 @@ resource "aws_iam_role" "lambda_execution_role" {
4087
5342
 
4088
5343
  # Attach basic execution policy to Lambda role
4089
5344
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
5345
+ for_each = local.operations
5346
+
4090
5347
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
4091
- role = aws_iam_role.lambda_execution_role.name
5348
+ role = aws_iam_role.lambda_execution_role[each.key].name
4092
5349
  }
4093
5350
 
4094
5351
  # Attach X-Ray tracing policy to Lambda role
4095
5352
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
5353
+ for_each = local.operations
5354
+
4096
5355
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
4097
- role = aws_iam_role.lambda_execution_role.name
5356
+ role = aws_iam_role.lambda_execution_role[each.key].name
4098
5357
  }
4099
5358
 
4100
5359
  # Attach VPC access policy to Lambda role when deployed into a VPC
4101
5360
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
4102
- count = var.enable_vpc ? 1 : 0
5361
+ for_each = var.enable_vpc ? local.operations : {}
5362
+
4103
5363
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
4104
- role = aws_iam_role.lambda_execution_role.name
5364
+ role = aws_iam_role.lambda_execution_role[each.key].name
4105
5365
  }
4106
5366
 
4107
5367
  # Additional IAM policies for Lambda (if provided)
4108
5368
  resource "aws_iam_role_policy" "lambda_additional_policies" {
4109
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
4110
- name = "CustomApiHandler-additional-policies-\${random_string.suffix.result}"
4111
- role = aws_iam_role.lambda_execution_role.id
5369
+ for_each = local.operations
5370
+
5371
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
5372
+ role = aws_iam_role.lambda_execution_role[each.key].id
4112
5373
 
4113
5374
  policy = jsonencode({
4114
5375
  Version = "2012-10-17"
4115
- Statement = local.lambda_policy_statements
5376
+ Statement = local.lambda_policy_document_statements
4116
5377
  })
4117
5378
  }
4118
5379
 
@@ -4128,6 +5389,15 @@ locals {
4128
5389
  Resource = ["\${var.appconfig_application_arn}/*"]
4129
5390
  }
4130
5391
  ] : [], var.additional_iam_policy_statements)
5392
+
5393
+ # IAM rejects an empty statement list, so fall back to a no-op deny
5394
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
5395
+ {
5396
+ Effect = "Deny"
5397
+ Action = ["appconfig:GetLatestConfiguration"]
5398
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
5399
+ }
5400
+ ]
4131
5401
  }
4132
5402
 
4133
5403
  # CloudWatch Log Group for Lambda
@@ -4135,8 +5405,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
4135
5405
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
4136
5406
  #checkov:skip=CKV_AWS_338:Log retention set to forever
4137
5407
  #checkov:skip=CKV_AWS_66:Log retention set to forever
4138
- name = "/aws/lambda/CustomApiHandler-\${random_string.suffix.result}"
4139
- tags = var.tags
5408
+ for_each = local.operations
5409
+
5410
+ name = "/aws/lambda/\${local.function_name[each.key]}"
5411
+ tags = var.tags
4140
5412
  }
4141
5413
 
4142
5414
 
@@ -4233,58 +5505,62 @@ resource "aws_lambda_permission" "authorizer_invoke" {
4233
5505
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
4234
5506
  }
4235
5507
 
4236
- # Create proxy resource (captures all paths)
4237
- resource "aws_api_gateway_resource" "proxy_resource" {
4238
- rest_api_id = module.rest_api.api_id
4239
- parent_id = module.rest_api.api_root_resource_id
4240
- path_part = "{proxy+}"
4241
- }
4242
-
4243
- # Lambda integration for REST API
4244
- resource "aws_api_gateway_integration" "lambda_integration" {
4245
- rest_api_id = module.rest_api.api_id
4246
- resource_id = aws_api_gateway_resource.proxy_resource.id
4247
- http_method = aws_api_gateway_method.proxy_method.http_method
4248
-
4249
- integration_http_method = "POST"
4250
- type = "AWS_PROXY"
4251
- uri = aws_lambda_function.api_lambda.invoke_arn
4252
-
4253
- depends_on = [aws_lambda_function.api_lambda]
4254
- }
4255
-
4256
- # Method for proxy integration
4257
- resource "aws_api_gateway_method" "proxy_method" {
5508
+ # Method per operation
5509
+ resource "aws_api_gateway_method" "operation_methods" {
4258
5510
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
5511
+ for_each = local.operations
5512
+
4259
5513
  rest_api_id = module.rest_api.api_id
4260
- resource_id = aws_api_gateway_resource.proxy_resource.id
4261
- http_method = "ANY"
5514
+ resource_id = local.operation_resource_id[each.key]
5515
+ http_method = upper(each.value.method)
4262
5516
 
4263
5517
  authorization = "CUSTOM"
4264
5518
  authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
4265
5519
 
5520
+ # Path parameters must be declared on the method
4266
5521
  request_parameters = {
4267
- "method.request.path.proxy" = true
5522
+ for segment in local.operation_segments[each.key] :
5523
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
5524
+ if startswith(segment, "{")
4268
5525
  }
4269
5526
 
4270
5527
  depends_on = [aws_api_gateway_authorizer.custom_authorizer]
4271
5528
  }
4272
5529
 
4273
- # OPTIONS method for CORS preflight
5530
+ # Lambda integration per operation
5531
+ resource "aws_api_gateway_integration" "lambda_integration" {
5532
+ for_each = local.operations
5533
+
5534
+ rest_api_id = module.rest_api.api_id
5535
+ resource_id = local.operation_resource_id[each.key]
5536
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
5537
+
5538
+ integration_http_method = "POST"
5539
+ type = "AWS_PROXY"
5540
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
5541
+
5542
+ depends_on = [aws_lambda_function.api_lambda]
5543
+ }
5544
+
5545
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
4274
5546
  resource "aws_api_gateway_method" "options_method" {
4275
5547
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
4276
5548
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
5549
+ for_each = local.resource_ids
5550
+
4277
5551
  rest_api_id = module.rest_api.api_id
4278
- resource_id = aws_api_gateway_resource.proxy_resource.id
5552
+ resource_id = each.value
4279
5553
  http_method = "OPTIONS"
4280
5554
  authorization = "NONE"
4281
5555
  }
4282
5556
 
4283
- # CORS integration for OPTIONS method
5557
+ # CORS integration for OPTIONS methods
4284
5558
  resource "aws_api_gateway_integration" "options_integration" {
5559
+ for_each = local.resource_ids
5560
+
4285
5561
  rest_api_id = module.rest_api.api_id
4286
- resource_id = aws_api_gateway_resource.proxy_resource.id
4287
- http_method = aws_api_gateway_method.options_method.http_method
5562
+ resource_id = each.value
5563
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
4288
5564
 
4289
5565
  type = "MOCK"
4290
5566
  request_templates = {
@@ -4292,11 +5568,13 @@ resource "aws_api_gateway_integration" "options_integration" {
4292
5568
  }
4293
5569
  }
4294
5570
 
4295
- # OPTIONS method response
5571
+ # OPTIONS method responses
4296
5572
  resource "aws_api_gateway_method_response" "options_response" {
5573
+ for_each = local.resource_ids
5574
+
4297
5575
  rest_api_id = module.rest_api.api_id
4298
- resource_id = aws_api_gateway_resource.proxy_resource.id
4299
- http_method = aws_api_gateway_method.options_method.http_method
5576
+ resource_id = each.value
5577
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
4300
5578
  status_code = "204"
4301
5579
 
4302
5580
  response_parameters = {
@@ -4306,18 +5584,22 @@ resource "aws_api_gateway_method_response" "options_response" {
4306
5584
  }
4307
5585
  }
4308
5586
 
4309
- # OPTIONS integration response
5587
+ # OPTIONS integration responses
4310
5588
  resource "aws_api_gateway_integration_response" "options_integration_response" {
5589
+ for_each = local.resource_ids
5590
+
4311
5591
  rest_api_id = module.rest_api.api_id
4312
- resource_id = aws_api_gateway_resource.proxy_resource.id
4313
- http_method = aws_api_gateway_method.options_method.http_method
4314
- status_code = aws_api_gateway_method_response.options_response.status_code
5592
+ resource_id = each.value
5593
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
5594
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
4315
5595
 
4316
5596
  response_parameters = {
4317
5597
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
4318
5598
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
4319
5599
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
4320
5600
  }
5601
+
5602
+ depends_on = [aws_api_gateway_integration.options_integration]
4321
5603
  }
4322
5604
 
4323
5605
  # API Gateway deployment
@@ -4325,13 +5607,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4325
5607
  rest_api_id = module.rest_api.api_id
4326
5608
 
4327
5609
  triggers = {
4328
- redeployment = sha1(jsonencode([
4329
- aws_api_gateway_resource.proxy_resource.id,
4330
- aws_api_gateway_method.proxy_method.id,
4331
- aws_api_gateway_integration.lambda_integration.id,
4332
- aws_api_gateway_method.options_method.id,
4333
- aws_api_gateway_integration.options_integration.id,
4334
- ]))
5610
+ redeployment = sha1(jsonencode(concat(
5611
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
5612
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
5613
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
5614
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
5615
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
5616
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
5617
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
5618
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
5619
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
5620
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
5621
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
5622
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
5623
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
5624
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
5625
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
5626
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
5627
+ [for m in aws_api_gateway_method.operation_methods : m.id],
5628
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
5629
+ [for m in aws_api_gateway_method.options_method : m.id],
5630
+ [for i in aws_api_gateway_integration.options_integration : i.id],
5631
+ )))
4335
5632
  }
4336
5633
 
4337
5634
  lifecycle {
@@ -4339,7 +5636,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4339
5636
  }
4340
5637
 
4341
5638
  depends_on = [
4342
- aws_api_gateway_method.proxy_method,
5639
+ aws_api_gateway_method.operation_methods,
4343
5640
  aws_api_gateway_integration.lambda_integration,
4344
5641
  aws_api_gateway_method.options_method,
4345
5642
  aws_api_gateway_integration.options_integration,
@@ -4468,11 +5765,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
4468
5765
 
4469
5766
  # Lambda permission for API Gateway to invoke the function
4470
5767
  resource "aws_lambda_permission" "api_gateway_invoke" {
5768
+ for_each = local.operations
5769
+
4471
5770
  statement_id = "AllowExecutionFromAPIGateway"
4472
5771
  action = "lambda:InvokeFunction"
4473
- function_name = aws_lambda_function.api_lambda.function_name
5772
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
4474
5773
  principal = "apigateway.amazonaws.com"
4475
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
5774
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
4476
5775
 
4477
5776
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
4478
5777
  }
@@ -4484,7 +5783,7 @@ module "add_url_to_runtime_config" {
4484
5783
 
4485
5784
  namespace = "connection"
4486
5785
  key = "apis"
4487
- value = { "CustomApi" = aws_api_gateway_stage.api_stage.invoke_url }
5786
+ value = { "CustomApi" = "\${aws_api_gateway_stage.api_stage.invoke_url}/" }
4488
5787
 
4489
5788
  depends_on = [aws_api_gateway_stage.api_stage]
4490
5789
  }
@@ -4537,83 +5836,83 @@ output "stage_id" {
4537
5836
  value = aws_api_gateway_stage.api_stage.id
4538
5837
  }
4539
5838
 
4540
- # Lambda Function Outputs
4541
- output "lambda_function_name" {
4542
- description = "Name of the Lambda function"
4543
- value = aws_lambda_function.api_lambda.function_name
5839
+ # Lambda Function Outputs, keyed by operation name
5840
+ output "operations" {
5841
+ description = "Names of the API operations, each with its own Lambda function"
5842
+ value = keys(local.operations)
4544
5843
  }
4545
5844
 
4546
- output "lambda_function_arn" {
4547
- description = "ARN of the Lambda function"
4548
- value = aws_lambda_function.api_lambda.arn
5845
+ output "lambda_function_names" {
5846
+ description = "Name of each operation's Lambda function, keyed by operation name"
5847
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
4549
5848
  }
4550
5849
 
4551
- output "lambda_invoke_arn" {
4552
- description = "Invoke ARN of the Lambda function"
4553
- value = aws_lambda_function.api_lambda.invoke_arn
5850
+ output "lambda_function_arns" {
5851
+ description = "ARN of each operation's Lambda function, keyed by operation name"
5852
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
4554
5853
  }
4555
5854
 
4556
- output "lambda_qualified_arn" {
4557
- description = "Qualified ARN of the Lambda function"
4558
- value = aws_lambda_function.api_lambda.qualified_arn
5855
+ output "lambda_invoke_arns" {
5856
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
5857
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
4559
5858
  }
4560
5859
 
4561
- output "lambda_version" {
4562
- description = "Version of the Lambda function"
4563
- value = aws_lambda_function.api_lambda.version
5860
+ output "lambda_qualified_arns" {
5861
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
5862
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
4564
5863
  }
4565
5864
 
4566
- output "lambda_source_code_hash" {
4567
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
4568
- value = aws_lambda_function.api_lambda.source_code_hash
5865
+ output "lambda_versions" {
5866
+ description = "Version of each operation's Lambda function, keyed by operation name"
5867
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
4569
5868
  }
4570
5869
 
4571
- output "lambda_source_code_size" {
4572
- description = "Size of the Lambda deployment package in bytes"
4573
- value = aws_lambda_function.api_lambda.source_code_size
5870
+ output "lambda_source_code_hash" {
5871
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
5872
+ value = data.archive_file.lambda_zip.output_base64sha256
4574
5873
  }
4575
5874
 
4576
- # IAM Role Outputs
4577
- output "lambda_execution_role_arn" {
4578
- description = "ARN of the Lambda execution role"
4579
- value = aws_iam_role.lambda_execution_role.arn
5875
+ # IAM Role Outputs, keyed by operation name
5876
+ output "lambda_execution_role_arns" {
5877
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
5878
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
4580
5879
  }
4581
5880
 
4582
- output "lambda_execution_role_name" {
4583
- description = "Name of the Lambda execution role"
4584
- value = aws_iam_role.lambda_execution_role.name
5881
+ output "lambda_execution_role_names" {
5882
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
5883
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
4585
5884
  }
4586
5885
 
4587
5886
  output "security_group_id" {
4588
- 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."
5887
+ 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."
4589
5888
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
4590
5889
  }
4591
5890
 
4592
- # Integration Outputs
4593
- output "integration_id" {
4594
- description = "ID of the Lambda integration"
4595
- value = aws_api_gateway_integration.lambda_integration.id
5891
+ # Integration Outputs, keyed by operation name
5892
+ output "integration_ids" {
5893
+ description = "ID of each operation's Lambda integration, keyed by operation name"
5894
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
4596
5895
  }
4597
5896
 
4598
- output "proxy_resource_id" {
4599
- description = "ID of the proxy resource"
4600
- value = aws_api_gateway_resource.proxy_resource.id
5897
+ output "method_ids" {
5898
+ description = "ID of each operation's API Gateway method, keyed by operation name"
5899
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
4601
5900
  }
4602
5901
 
4603
- output "proxy_method_id" {
4604
- description = "ID of the proxy method"
4605
- value = aws_api_gateway_method.proxy_method.id
5902
+ output "resource_ids" {
5903
+ description = "ID of each API Gateway resource, keyed by its path"
5904
+ value = local.resource_ids
4606
5905
  }
4607
5906
 
4608
- # CloudWatch Log Groups
4609
- output "lambda_log_group_name" {
4610
- description = "Name of the Lambda CloudWatch log group"
4611
- value = aws_cloudwatch_log_group.lambda_logs.name
5907
+ # CloudWatch Log Groups, keyed by operation name
5908
+ output "lambda_log_group_names" {
5909
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
5910
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
4612
5911
  }
4613
5912
 
4614
- output "lambda_log_group_arn" {
4615
- description = "ARN of the Lambda CloudWatch log group"
4616
- value = aws_cloudwatch_log_group.lambda_logs.arn
5913
+ output "lambda_log_group_arns" {
5914
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
5915
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
4617
5916
  }
4618
5917
  "
4619
5918
  `;