@aws/nx-plugin 1.0.0-rc.84 → 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 (45) hide show
  1. package/generators.json +7 -0
  2. package/migrations.json +6 -1
  3. package/package.json +1 -1
  4. package/src/internal/test-matrix/generator.js +16 -0
  5. package/src/internal/test-matrix/generator.js.map +1 -1
  6. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/metadata.json +3 -0
  7. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.d.ts +6 -0
  8. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js +146 -0
  9. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js.map +1 -0
  10. package/src/open-api/json-metadata/generator.d.ts +24 -0
  11. package/src/open-api/json-metadata/generator.js +37 -0
  12. package/src/open-api/json-metadata/generator.js.map +1 -0
  13. package/src/open-api/json-metadata/schema.d.js +6 -0
  14. package/src/open-api/json-metadata/schema.d.js.map +1 -0
  15. package/src/open-api/json-metadata/schema.d.ts +9 -0
  16. package/src/open-api/json-metadata/schema.json +20 -0
  17. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +1604 -559
  18. package/src/py/fast-api/generator.js +2 -1
  19. package/src/py/fast-api/generator.js.map +1 -1
  20. package/src/sdk/open-api.d.ts +2 -0
  21. package/src/sdk/open-api.js +2 -1
  22. package/src/sdk/open-api.js.map +1 -1
  23. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +1548 -440
  24. package/src/smithy/ts/api/generator.js +2 -1
  25. package/src/smithy/ts/api/generator.js.map +1 -1
  26. package/src/terraform/project/files/application/bootstrap/providers.tf.template +2 -0
  27. package/src/terraform/project/files/application/scripts/bootstrap-destroy.ts.template +111 -0
  28. package/src/terraform/project/files/application/src/providers.tf.template +2 -0
  29. package/src/terraform/project/files/checkov/checkov.yml.template +8 -0
  30. package/src/terraform/project/generator.js +47 -4
  31. package/src/terraform/project/generator.js.map +1 -1
  32. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +1552 -525
  33. package/src/trpc/backend/files-operations/scripts/generate-operations.ts.template +40 -0
  34. package/src/trpc/backend/generator.js +10 -0
  35. package/src/trpc/backend/generator.js.map +1 -1
  36. package/src/utils/api-constructs/api-constructs.js +11 -0
  37. package/src/utils/api-constructs/api-constructs.js.map +1 -1
  38. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +243 -3
  39. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +428 -1
  40. package/src/utils/api-constructs/open-api-metadata.d.ts +20 -3
  41. package/src/utils/api-constructs/open-api-metadata.js +64 -8
  42. package/src/utils/api-constructs/open-api-metadata.js.map +1 -1
  43. package/src/utils/api-constructs/trpc-operations-metadata.d.ts +21 -0
  44. package/src/utils/api-constructs/trpc-operations-metadata.js +55 -0
  45. package/src/utils/api-constructs/trpc-operations-metadata.js.map +1 -0
@@ -840,6 +840,235 @@ resource "random_string" "suffix" {
840
840
  upper = false
841
841
  }
842
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
+
843
1072
  # Resources
844
1073
 
845
1074
  # Create Lambda ZIP file from the bundle directory
@@ -914,8 +1143,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
914
1143
  description = "Allow outbound HTTPS to AWS service endpoints"
915
1144
  }
916
1145
 
917
- # Lambda function
918
1146
  resource "aws_lambda_function" "api_lambda" {
1147
+ # One lambda function per operation, each serving just that operation
1148
+ for_each = local.operations
1149
+
919
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
920
1151
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
921
1152
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -924,8 +1155,8 @@ resource "aws_lambda_function" "api_lambda" {
924
1155
  s3_bucket = aws_s3_object.lambda_zip.bucket
925
1156
  s3_key = aws_s3_object.lambda_zip.key
926
1157
  s3_object_version = aws_s3_object.lambda_zip.version_id
927
- function_name = "TestApiHandler-\${random_string.suffix.result}"
928
- 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
929
1160
  handler = "index.handler"
930
1161
  runtime = "nodejs22.x"
931
1162
  timeout = 30
@@ -961,7 +1192,9 @@ resource "aws_lambda_function" "api_lambda" {
961
1192
 
962
1193
  # IAM role for Lambda execution
963
1194
  resource "aws_iam_role" "lambda_execution_role" {
964
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
1195
+ for_each = local.operations
1196
+
1197
+ name = local.role_name[each.key]
965
1198
 
966
1199
  assume_role_policy = jsonencode({
967
1200
  Version = "2012-10-17"
@@ -981,32 +1214,38 @@ resource "aws_iam_role" "lambda_execution_role" {
981
1214
 
982
1215
  # Attach basic execution policy to Lambda role
983
1216
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
1217
+ for_each = local.operations
1218
+
984
1219
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
985
- role = aws_iam_role.lambda_execution_role.name
1220
+ role = aws_iam_role.lambda_execution_role[each.key].name
986
1221
  }
987
1222
 
988
1223
  # Attach X-Ray tracing policy to Lambda role
989
1224
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
1225
+ for_each = local.operations
1226
+
990
1227
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
991
- role = aws_iam_role.lambda_execution_role.name
1228
+ role = aws_iam_role.lambda_execution_role[each.key].name
992
1229
  }
993
1230
 
994
1231
  # Attach VPC access policy to Lambda role when deployed into a VPC
995
1232
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
996
- count = var.enable_vpc ? 1 : 0
1233
+ for_each = var.enable_vpc ? local.operations : {}
1234
+
997
1235
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
998
- role = aws_iam_role.lambda_execution_role.name
1236
+ role = aws_iam_role.lambda_execution_role[each.key].name
999
1237
  }
1000
1238
 
1001
1239
  # Additional IAM policies for Lambda (if provided)
1002
1240
  resource "aws_iam_role_policy" "lambda_additional_policies" {
1003
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
1004
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
1005
- 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
1006
1245
 
1007
1246
  policy = jsonencode({
1008
1247
  Version = "2012-10-17"
1009
- Statement = local.lambda_policy_statements
1248
+ Statement = local.lambda_policy_document_statements
1010
1249
  })
1011
1250
  }
1012
1251
 
@@ -1022,6 +1261,15 @@ locals {
1022
1261
  Resource = ["\${var.appconfig_application_arn}/*"]
1023
1262
  }
1024
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
+ ]
1025
1273
  }
1026
1274
 
1027
1275
  # CloudWatch Log Group for Lambda
@@ -1029,8 +1277,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
1029
1277
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
1030
1278
  #checkov:skip=CKV_AWS_338:Log retention set to forever
1031
1279
  #checkov:skip=CKV_AWS_66:Log retention set to forever
1032
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
1033
- tags = var.tags
1280
+ for_each = local.operations
1281
+
1282
+ name = "/aws/lambda/\${local.function_name[each.key]}"
1283
+ tags = var.tags
1034
1284
  }
1035
1285
 
1036
1286
 
@@ -1127,58 +1377,62 @@ resource "aws_lambda_permission" "authorizer_invoke" {
1127
1377
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
1128
1378
  }
1129
1379
 
1130
- # Create proxy resource (captures all paths)
1131
- resource "aws_api_gateway_resource" "proxy_resource" {
1132
- rest_api_id = module.rest_api.api_id
1133
- parent_id = module.rest_api.api_root_resource_id
1134
- path_part = "{proxy+}"
1135
- }
1136
-
1137
- # Lambda integration for REST API
1138
- resource "aws_api_gateway_integration" "lambda_integration" {
1139
- rest_api_id = module.rest_api.api_id
1140
- resource_id = aws_api_gateway_resource.proxy_resource.id
1141
- http_method = aws_api_gateway_method.proxy_method.http_method
1142
-
1143
- integration_http_method = "POST"
1144
- type = "AWS_PROXY"
1145
- uri = aws_lambda_function.api_lambda.invoke_arn
1146
-
1147
- depends_on = [aws_lambda_function.api_lambda]
1148
- }
1149
-
1150
- # Method for proxy integration
1151
- resource "aws_api_gateway_method" "proxy_method" {
1380
+ # Method per operation
1381
+ resource "aws_api_gateway_method" "operation_methods" {
1152
1382
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
1383
+ for_each = local.operations
1384
+
1153
1385
  rest_api_id = module.rest_api.api_id
1154
- resource_id = aws_api_gateway_resource.proxy_resource.id
1155
- http_method = "ANY"
1386
+ resource_id = local.operation_resource_id[each.key]
1387
+ http_method = upper(each.value.method)
1156
1388
 
1157
1389
  authorization = "CUSTOM"
1158
1390
  authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
1159
1391
 
1392
+ # Path parameters must be declared on the method
1160
1393
  request_parameters = {
1161
- "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, "{")
1162
1397
  }
1163
1398
 
1164
1399
  depends_on = [aws_api_gateway_authorizer.custom_authorizer]
1165
1400
  }
1166
1401
 
1167
- # 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
1168
1418
  resource "aws_api_gateway_method" "options_method" {
1169
1419
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
1170
1420
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
1421
+ for_each = local.resource_ids
1422
+
1171
1423
  rest_api_id = module.rest_api.api_id
1172
- resource_id = aws_api_gateway_resource.proxy_resource.id
1424
+ resource_id = each.value
1173
1425
  http_method = "OPTIONS"
1174
1426
  authorization = "NONE"
1175
1427
  }
1176
1428
 
1177
- # CORS integration for OPTIONS method
1429
+ # CORS integration for OPTIONS methods
1178
1430
  resource "aws_api_gateway_integration" "options_integration" {
1431
+ for_each = local.resource_ids
1432
+
1179
1433
  rest_api_id = module.rest_api.api_id
1180
- resource_id = aws_api_gateway_resource.proxy_resource.id
1181
- 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
1182
1436
 
1183
1437
  type = "MOCK"
1184
1438
  request_templates = {
@@ -1186,11 +1440,13 @@ resource "aws_api_gateway_integration" "options_integration" {
1186
1440
  }
1187
1441
  }
1188
1442
 
1189
- # OPTIONS method response
1443
+ # OPTIONS method responses
1190
1444
  resource "aws_api_gateway_method_response" "options_response" {
1445
+ for_each = local.resource_ids
1446
+
1191
1447
  rest_api_id = module.rest_api.api_id
1192
- resource_id = aws_api_gateway_resource.proxy_resource.id
1193
- 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
1194
1450
  status_code = "204"
1195
1451
 
1196
1452
  response_parameters = {
@@ -1200,18 +1456,22 @@ resource "aws_api_gateway_method_response" "options_response" {
1200
1456
  }
1201
1457
  }
1202
1458
 
1203
- # OPTIONS integration response
1459
+ # OPTIONS integration responses
1204
1460
  resource "aws_api_gateway_integration_response" "options_integration_response" {
1461
+ for_each = local.resource_ids
1462
+
1205
1463
  rest_api_id = module.rest_api.api_id
1206
- resource_id = aws_api_gateway_resource.proxy_resource.id
1207
- http_method = aws_api_gateway_method.options_method.http_method
1208
- 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
1209
1467
 
1210
1468
  response_parameters = {
1211
1469
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
1212
1470
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
1213
1471
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
1214
1472
  }
1473
+
1474
+ depends_on = [aws_api_gateway_integration.options_integration]
1215
1475
  }
1216
1476
 
1217
1477
  # API Gateway deployment
@@ -1219,13 +1479,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
1219
1479
  rest_api_id = module.rest_api.api_id
1220
1480
 
1221
1481
  triggers = {
1222
- redeployment = sha1(jsonencode([
1223
- aws_api_gateway_resource.proxy_resource.id,
1224
- aws_api_gateway_method.proxy_method.id,
1225
- aws_api_gateway_integration.lambda_integration.id,
1226
- aws_api_gateway_method.options_method.id,
1227
- aws_api_gateway_integration.options_integration.id,
1228
- ]))
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
+ )))
1229
1504
  }
1230
1505
 
1231
1506
  lifecycle {
@@ -1233,7 +1508,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
1233
1508
  }
1234
1509
 
1235
1510
  depends_on = [
1236
- aws_api_gateway_method.proxy_method,
1511
+ aws_api_gateway_method.operation_methods,
1237
1512
  aws_api_gateway_integration.lambda_integration,
1238
1513
  aws_api_gateway_method.options_method,
1239
1514
  aws_api_gateway_integration.options_integration,
@@ -1362,11 +1637,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
1362
1637
 
1363
1638
  # Lambda permission for API Gateway to invoke the function
1364
1639
  resource "aws_lambda_permission" "api_gateway_invoke" {
1640
+ for_each = local.operations
1641
+
1365
1642
  statement_id = "AllowExecutionFromAPIGateway"
1366
1643
  action = "lambda:InvokeFunction"
1367
- function_name = aws_lambda_function.api_lambda.function_name
1644
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1368
1645
  principal = "apigateway.amazonaws.com"
1369
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
1646
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
1370
1647
 
1371
1648
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
1372
1649
  }
@@ -1431,83 +1708,83 @@ output "stage_id" {
1431
1708
  value = aws_api_gateway_stage.api_stage.id
1432
1709
  }
1433
1710
 
1434
- # Lambda Function Outputs
1435
- output "lambda_function_name" {
1436
- description = "Name of the Lambda function"
1437
- 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)
1438
1715
  }
1439
1716
 
1440
- output "lambda_function_arn" {
1441
- description = "ARN of the Lambda function"
1442
- 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 }
1443
1720
  }
1444
1721
 
1445
- output "lambda_invoke_arn" {
1446
- description = "Invoke ARN of the Lambda function"
1447
- 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 }
1448
1725
  }
1449
1726
 
1450
- output "lambda_qualified_arn" {
1451
- description = "Qualified ARN of the Lambda function"
1452
- 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 }
1453
1730
  }
1454
1731
 
1455
- output "lambda_version" {
1456
- description = "Version of the Lambda function"
1457
- 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 }
1458
1735
  }
1459
1736
 
1460
- output "lambda_source_code_hash" {
1461
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
1462
- 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 }
1463
1740
  }
1464
1741
 
1465
- output "lambda_source_code_size" {
1466
- description = "Size of the Lambda deployment package in bytes"
1467
- 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
1468
1745
  }
1469
1746
 
1470
- # IAM Role Outputs
1471
- output "lambda_execution_role_arn" {
1472
- description = "ARN of the Lambda execution role"
1473
- 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 }
1474
1751
  }
1475
1752
 
1476
- output "lambda_execution_role_name" {
1477
- description = "Name of the Lambda execution role"
1478
- 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 }
1479
1756
  }
1480
1757
 
1481
1758
  output "security_group_id" {
1482
- 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."
1483
1760
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
1484
1761
  }
1485
1762
 
1486
- # Integration Outputs
1487
- output "integration_id" {
1488
- description = "ID of the Lambda integration"
1489
- 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 }
1490
1767
  }
1491
1768
 
1492
- output "proxy_resource_id" {
1493
- description = "ID of the proxy resource"
1494
- 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 }
1495
1772
  }
1496
1773
 
1497
- output "proxy_method_id" {
1498
- description = "ID of the proxy method"
1499
- 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
1500
1777
  }
1501
1778
 
1502
- # CloudWatch Log Groups
1503
- output "lambda_log_group_name" {
1504
- description = "Name of the Lambda CloudWatch log group"
1505
- 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 }
1506
1783
  }
1507
1784
 
1508
- output "lambda_log_group_arn" {
1509
- description = "ARN of the Lambda CloudWatch log group"
1510
- 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 }
1511
1788
  }
1512
1789
  "
1513
1790
  `;
@@ -2399,6 +2676,235 @@ resource "random_string" "suffix" {
2399
2676
  upper = false
2400
2677
  }
2401
2678
 
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
+
2402
2908
  # Resources
2403
2909
 
2404
2910
  # Create Lambda ZIP file from the bundle directory
@@ -2473,8 +2979,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
2473
2979
  description = "Allow outbound HTTPS to AWS service endpoints"
2474
2980
  }
2475
2981
 
2476
- # Lambda function
2477
2982
  resource "aws_lambda_function" "api_lambda" {
2983
+ # One lambda function per operation, each serving just that operation
2984
+ for_each = local.operations
2985
+
2478
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
2479
2987
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
2480
2988
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -2483,8 +2991,8 @@ resource "aws_lambda_function" "api_lambda" {
2483
2991
  s3_bucket = aws_s3_object.lambda_zip.bucket
2484
2992
  s3_key = aws_s3_object.lambda_zip.key
2485
2993
  s3_object_version = aws_s3_object.lambda_zip.version_id
2486
- function_name = "TestApiHandler-\${random_string.suffix.result}"
2487
- 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
2488
2996
  handler = "index.handler"
2489
2997
  runtime = "nodejs22.x"
2490
2998
  timeout = 30
@@ -2520,7 +3028,9 @@ resource "aws_lambda_function" "api_lambda" {
2520
3028
 
2521
3029
  # IAM role for Lambda execution
2522
3030
  resource "aws_iam_role" "lambda_execution_role" {
2523
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
3031
+ for_each = local.operations
3032
+
3033
+ name = local.role_name[each.key]
2524
3034
 
2525
3035
  assume_role_policy = jsonencode({
2526
3036
  Version = "2012-10-17"
@@ -2540,32 +3050,38 @@ resource "aws_iam_role" "lambda_execution_role" {
2540
3050
 
2541
3051
  # Attach basic execution policy to Lambda role
2542
3052
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
3053
+ for_each = local.operations
3054
+
2543
3055
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
2544
- role = aws_iam_role.lambda_execution_role.name
3056
+ role = aws_iam_role.lambda_execution_role[each.key].name
2545
3057
  }
2546
3058
 
2547
3059
  # Attach X-Ray tracing policy to Lambda role
2548
3060
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
3061
+ for_each = local.operations
3062
+
2549
3063
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
2550
- role = aws_iam_role.lambda_execution_role.name
3064
+ role = aws_iam_role.lambda_execution_role[each.key].name
2551
3065
  }
2552
3066
 
2553
3067
  # Attach VPC access policy to Lambda role when deployed into a VPC
2554
3068
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
2555
- count = var.enable_vpc ? 1 : 0
3069
+ for_each = var.enable_vpc ? local.operations : {}
3070
+
2556
3071
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
2557
- role = aws_iam_role.lambda_execution_role.name
3072
+ role = aws_iam_role.lambda_execution_role[each.key].name
2558
3073
  }
2559
3074
 
2560
3075
  # Additional IAM policies for Lambda (if provided)
2561
3076
  resource "aws_iam_role_policy" "lambda_additional_policies" {
2562
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
2563
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
2564
- 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
2565
3081
 
2566
3082
  policy = jsonencode({
2567
3083
  Version = "2012-10-17"
2568
- Statement = local.lambda_policy_statements
3084
+ Statement = local.lambda_policy_document_statements
2569
3085
  })
2570
3086
  }
2571
3087
 
@@ -2581,6 +3097,15 @@ locals {
2581
3097
  Resource = ["\${var.appconfig_application_arn}/*"]
2582
3098
  }
2583
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
+ ]
2584
3109
  }
2585
3110
 
2586
3111
  # CloudWatch Log Group for Lambda
@@ -2588,8 +3113,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
2588
3113
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
2589
3114
  #checkov:skip=CKV_AWS_338:Log retention set to forever
2590
3115
  #checkov:skip=CKV_AWS_66:Log retention set to forever
2591
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
2592
- tags = var.tags
3116
+ for_each = local.operations
3117
+
3118
+ name = "/aws/lambda/\${local.function_name[each.key]}"
3119
+ tags = var.tags
2593
3120
  }
2594
3121
 
2595
3122
 
@@ -2602,32 +3129,14 @@ resource "aws_api_gateway_authorizer" "cognito_authorizer" {
2602
3129
  identity_source = "method.request.header.Authorization"
2603
3130
  }
2604
3131
 
2605
- # Create proxy resource (captures all paths)
2606
- resource "aws_api_gateway_resource" "proxy_resource" {
2607
- rest_api_id = module.rest_api.api_id
2608
- parent_id = module.rest_api.api_root_resource_id
2609
- path_part = "{proxy+}"
2610
- }
2611
-
2612
- # Lambda integration for REST API
2613
- resource "aws_api_gateway_integration" "lambda_integration" {
2614
- rest_api_id = module.rest_api.api_id
2615
- resource_id = aws_api_gateway_resource.proxy_resource.id
2616
- http_method = aws_api_gateway_method.proxy_method.http_method
2617
-
2618
- integration_http_method = "POST"
2619
- type = "AWS_PROXY"
2620
- uri = aws_lambda_function.api_lambda.invoke_arn
2621
-
2622
- depends_on = [aws_lambda_function.api_lambda]
2623
- }
2624
-
2625
- # Method for proxy integration
2626
- resource "aws_api_gateway_method" "proxy_method" {
3132
+ # Method per operation
3133
+ resource "aws_api_gateway_method" "operation_methods" {
2627
3134
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
3135
+ for_each = local.operations
3136
+
2628
3137
  rest_api_id = module.rest_api.api_id
2629
- resource_id = aws_api_gateway_resource.proxy_resource.id
2630
- http_method = "ANY"
3138
+ resource_id = local.operation_resource_id[each.key]
3139
+ http_method = upper(each.value.method)
2631
3140
 
2632
3141
  authorization = "COGNITO_USER_POOLS"
2633
3142
  authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
@@ -2635,40 +3144,64 @@ resource "aws_api_gateway_method" "proxy_method" {
2635
3144
  # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
2636
3145
  authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
2637
3146
 
3147
+ # Path parameters must be declared on the method
2638
3148
  request_parameters = {
2639
- "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, "{")
2640
3152
  }
2641
3153
 
2642
3154
  depends_on = [aws_api_gateway_authorizer.cognito_authorizer]
2643
3155
  }
2644
3156
 
2645
- # OPTIONS method for CORS preflight
2646
- resource "aws_api_gateway_method" "options_method" {
2647
- #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
2648
- #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
2649
- rest_api_id = module.rest_api.api_id
2650
- resource_id = aws_api_gateway_resource.proxy_resource.id
2651
- http_method = "OPTIONS"
2652
- authorization = "NONE"
2653
- }
3157
+ # Lambda integration per operation
3158
+ resource "aws_api_gateway_integration" "lambda_integration" {
3159
+ for_each = local.operations
2654
3160
 
2655
- # CORS integration for OPTIONS method
2656
- resource "aws_api_gateway_integration" "options_integration" {
2657
3161
  rest_api_id = module.rest_api.api_id
2658
- resource_id = aws_api_gateway_resource.proxy_resource.id
2659
- http_method = aws_api_gateway_method.options_method.http_method
3162
+ resource_id = local.operation_resource_id[each.key]
3163
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
2660
3164
 
2661
- type = "MOCK"
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
3173
+ resource "aws_api_gateway_method" "options_method" {
3174
+ #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
3175
+ #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
3176
+ for_each = local.resource_ids
3177
+
3178
+ rest_api_id = module.rest_api.api_id
3179
+ resource_id = each.value
3180
+ http_method = "OPTIONS"
3181
+ authorization = "NONE"
3182
+ }
3183
+
3184
+ # CORS integration for OPTIONS methods
3185
+ resource "aws_api_gateway_integration" "options_integration" {
3186
+ for_each = local.resource_ids
3187
+
3188
+ rest_api_id = module.rest_api.api_id
3189
+ resource_id = each.value
3190
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
3191
+
3192
+ type = "MOCK"
2662
3193
  request_templates = {
2663
3194
  "application/json" = "{\\"statusCode\\": 204}"
2664
3195
  }
2665
3196
  }
2666
3197
 
2667
- # OPTIONS method response
3198
+ # OPTIONS method responses
2668
3199
  resource "aws_api_gateway_method_response" "options_response" {
3200
+ for_each = local.resource_ids
3201
+
2669
3202
  rest_api_id = module.rest_api.api_id
2670
- resource_id = aws_api_gateway_resource.proxy_resource.id
2671
- 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
2672
3205
  status_code = "204"
2673
3206
 
2674
3207
  response_parameters = {
@@ -2678,18 +3211,22 @@ resource "aws_api_gateway_method_response" "options_response" {
2678
3211
  }
2679
3212
  }
2680
3213
 
2681
- # OPTIONS integration response
3214
+ # OPTIONS integration responses
2682
3215
  resource "aws_api_gateway_integration_response" "options_integration_response" {
3216
+ for_each = local.resource_ids
3217
+
2683
3218
  rest_api_id = module.rest_api.api_id
2684
- resource_id = aws_api_gateway_resource.proxy_resource.id
2685
- http_method = aws_api_gateway_method.options_method.http_method
2686
- 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
2687
3222
 
2688
3223
  response_parameters = {
2689
3224
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
2690
3225
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
2691
3226
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
2692
3227
  }
3228
+
3229
+ depends_on = [aws_api_gateway_integration.options_integration]
2693
3230
  }
2694
3231
 
2695
3232
  # API Gateway deployment
@@ -2697,13 +3234,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
2697
3234
  rest_api_id = module.rest_api.api_id
2698
3235
 
2699
3236
  triggers = {
2700
- redeployment = sha1(jsonencode([
2701
- aws_api_gateway_resource.proxy_resource.id,
2702
- aws_api_gateway_method.proxy_method.id,
2703
- aws_api_gateway_integration.lambda_integration.id,
2704
- aws_api_gateway_method.options_method.id,
2705
- aws_api_gateway_integration.options_integration.id,
2706
- ]))
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
+ )))
2707
3259
  }
2708
3260
 
2709
3261
  lifecycle {
@@ -2711,7 +3263,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
2711
3263
  }
2712
3264
 
2713
3265
  depends_on = [
2714
- aws_api_gateway_method.proxy_method,
3266
+ aws_api_gateway_method.operation_methods,
2715
3267
  aws_api_gateway_integration.lambda_integration,
2716
3268
  aws_api_gateway_method.options_method,
2717
3269
  aws_api_gateway_integration.options_integration,
@@ -2839,11 +3391,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
2839
3391
 
2840
3392
  # Lambda permission for API Gateway to invoke the function
2841
3393
  resource "aws_lambda_permission" "api_gateway_invoke" {
3394
+ for_each = local.operations
3395
+
2842
3396
  statement_id = "AllowExecutionFromAPIGateway"
2843
3397
  action = "lambda:InvokeFunction"
2844
- function_name = aws_lambda_function.api_lambda.function_name
3398
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
2845
3399
  principal = "apigateway.amazonaws.com"
2846
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
3400
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
2847
3401
 
2848
3402
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
2849
3403
  }
@@ -2908,83 +3462,83 @@ output "stage_id" {
2908
3462
  value = aws_api_gateway_stage.api_stage.id
2909
3463
  }
2910
3464
 
2911
- # Lambda Function Outputs
2912
- output "lambda_function_name" {
2913
- description = "Name of the Lambda function"
2914
- 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)
2915
3469
  }
2916
3470
 
2917
- output "lambda_function_arn" {
2918
- description = "ARN of the Lambda function"
2919
- 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 }
2920
3474
  }
2921
3475
 
2922
- output "lambda_invoke_arn" {
2923
- description = "Invoke ARN of the Lambda function"
2924
- 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 }
2925
3479
  }
2926
3480
 
2927
- output "lambda_qualified_arn" {
2928
- description = "Qualified ARN of the Lambda function"
2929
- 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 }
2930
3484
  }
2931
3485
 
2932
- output "lambda_version" {
2933
- description = "Version of the Lambda function"
2934
- 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 }
2935
3489
  }
2936
3490
 
2937
- output "lambda_source_code_hash" {
2938
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
2939
- 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 }
2940
3494
  }
2941
3495
 
2942
- output "lambda_source_code_size" {
2943
- description = "Size of the Lambda deployment package in bytes"
2944
- 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
2945
3499
  }
2946
3500
 
2947
- # IAM Role Outputs
2948
- output "lambda_execution_role_arn" {
2949
- description = "ARN of the Lambda execution role"
2950
- 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 }
2951
3505
  }
2952
3506
 
2953
- output "lambda_execution_role_name" {
2954
- description = "Name of the Lambda execution role"
2955
- 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 }
2956
3510
  }
2957
3511
 
2958
3512
  output "security_group_id" {
2959
- 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."
2960
3514
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
2961
3515
  }
2962
3516
 
2963
- # Integration Outputs
2964
- output "integration_id" {
2965
- description = "ID of the Lambda integration"
2966
- 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 }
2967
3521
  }
2968
3522
 
2969
- output "proxy_resource_id" {
2970
- description = "ID of the proxy resource"
2971
- 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 }
2972
3526
  }
2973
3527
 
2974
- output "proxy_method_id" {
2975
- description = "ID of the proxy method"
2976
- 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
2977
3531
  }
2978
3532
 
2979
- # CloudWatch Log Groups
2980
- output "lambda_log_group_name" {
2981
- description = "Name of the Lambda CloudWatch log group"
2982
- 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 }
2983
3537
  }
2984
3538
 
2985
- output "lambda_log_group_arn" {
2986
- description = "ARN of the Lambda CloudWatch log group"
2987
- 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 }
2988
3542
  }
2989
3543
  ",
2990
3544
  }
@@ -3425,6 +3979,235 @@ resource "random_string" "suffix" {
3425
3979
  upper = false
3426
3980
  }
3427
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
+
3428
4211
  # Resources
3429
4212
 
3430
4213
  # Create Lambda ZIP file from the bundle directory
@@ -3499,8 +4282,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
3499
4282
  description = "Allow outbound HTTPS to AWS service endpoints"
3500
4283
  }
3501
4284
 
3502
- # Lambda function
3503
4285
  resource "aws_lambda_function" "api_lambda" {
4286
+ # One lambda function per operation, each serving just that operation
4287
+ for_each = local.operations
4288
+
3504
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
3505
4290
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
3506
4291
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -3509,8 +4294,8 @@ resource "aws_lambda_function" "api_lambda" {
3509
4294
  s3_bucket = aws_s3_object.lambda_zip.bucket
3510
4295
  s3_key = aws_s3_object.lambda_zip.key
3511
4296
  s3_object_version = aws_s3_object.lambda_zip.version_id
3512
- function_name = "TestApiHandler-\${random_string.suffix.result}"
3513
- 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
3514
4299
  handler = "index.handler"
3515
4300
  runtime = "nodejs22.x"
3516
4301
  timeout = 30
@@ -3546,7 +4331,9 @@ resource "aws_lambda_function" "api_lambda" {
3546
4331
 
3547
4332
  # IAM role for Lambda execution
3548
4333
  resource "aws_iam_role" "lambda_execution_role" {
3549
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
4334
+ for_each = local.operations
4335
+
4336
+ name = local.role_name[each.key]
3550
4337
 
3551
4338
  assume_role_policy = jsonencode({
3552
4339
  Version = "2012-10-17"
@@ -3566,32 +4353,38 @@ resource "aws_iam_role" "lambda_execution_role" {
3566
4353
 
3567
4354
  # Attach basic execution policy to Lambda role
3568
4355
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
4356
+ for_each = local.operations
4357
+
3569
4358
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
3570
- role = aws_iam_role.lambda_execution_role.name
4359
+ role = aws_iam_role.lambda_execution_role[each.key].name
3571
4360
  }
3572
4361
 
3573
4362
  # Attach X-Ray tracing policy to Lambda role
3574
4363
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
4364
+ for_each = local.operations
4365
+
3575
4366
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
3576
- role = aws_iam_role.lambda_execution_role.name
4367
+ role = aws_iam_role.lambda_execution_role[each.key].name
3577
4368
  }
3578
4369
 
3579
4370
  # Attach VPC access policy to Lambda role when deployed into a VPC
3580
4371
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
3581
- count = var.enable_vpc ? 1 : 0
4372
+ for_each = var.enable_vpc ? local.operations : {}
4373
+
3582
4374
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
3583
- role = aws_iam_role.lambda_execution_role.name
4375
+ role = aws_iam_role.lambda_execution_role[each.key].name
3584
4376
  }
3585
4377
 
3586
4378
  # Additional IAM policies for Lambda (if provided)
3587
4379
  resource "aws_iam_role_policy" "lambda_additional_policies" {
3588
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
3589
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
3590
- 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
3591
4384
 
3592
4385
  policy = jsonencode({
3593
4386
  Version = "2012-10-17"
3594
- Statement = local.lambda_policy_statements
4387
+ Statement = local.lambda_policy_document_statements
3595
4388
  })
3596
4389
  }
3597
4390
 
@@ -3607,6 +4400,15 @@ locals {
3607
4400
  Resource = ["\${var.appconfig_application_arn}/*"]
3608
4401
  }
3609
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
+ ]
3610
4412
  }
3611
4413
 
3612
4414
  # CloudWatch Log Group for Lambda
@@ -3614,63 +4416,69 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
3614
4416
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3615
4417
  #checkov:skip=CKV_AWS_338:Log retention set to forever
3616
4418
  #checkov:skip=CKV_AWS_66:Log retention set to forever
3617
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
3618
- tags = var.tags
3619
- }
3620
-
4419
+ for_each = local.operations
3621
4420
 
3622
-
3623
- # Create proxy resource (captures all paths)
3624
- resource "aws_api_gateway_resource" "proxy_resource" {
3625
- rest_api_id = module.rest_api.api_id
3626
- parent_id = module.rest_api.api_root_resource_id
3627
- path_part = "{proxy+}"
4421
+ name = "/aws/lambda/\${local.function_name[each.key]}"
4422
+ tags = var.tags
3628
4423
  }
3629
4424
 
3630
- # Lambda integration for REST API
3631
- resource "aws_api_gateway_integration" "lambda_integration" {
3632
- rest_api_id = module.rest_api.api_id
3633
- resource_id = aws_api_gateway_resource.proxy_resource.id
3634
- http_method = aws_api_gateway_method.proxy_method.http_method
3635
-
3636
- integration_http_method = "POST"
3637
- type = "AWS_PROXY"
3638
- uri = aws_lambda_function.api_lambda.invoke_arn
3639
4425
 
3640
- depends_on = [aws_lambda_function.api_lambda]
3641
- }
3642
4426
 
3643
- # Method for proxy integration
3644
- resource "aws_api_gateway_method" "proxy_method" {
4427
+ # Method per operation
4428
+ resource "aws_api_gateway_method" "operation_methods" {
3645
4429
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
4430
+ for_each = local.operations
4431
+
3646
4432
  rest_api_id = module.rest_api.api_id
3647
- resource_id = aws_api_gateway_resource.proxy_resource.id
3648
- http_method = "ANY"
4433
+ resource_id = local.operation_resource_id[each.key]
4434
+ http_method = upper(each.value.method)
3649
4435
 
3650
4436
  authorization = "AWS_IAM"
3651
4437
 
4438
+ # Path parameters must be declared on the method
3652
4439
  request_parameters = {
3653
- "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, "{")
3654
4443
  }
3655
4444
 
3656
4445
  depends_on = []
3657
4446
  }
3658
4447
 
3659
- # 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
3660
4464
  resource "aws_api_gateway_method" "options_method" {
3661
4465
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
3662
4466
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
4467
+ for_each = local.resource_ids
4468
+
3663
4469
  rest_api_id = module.rest_api.api_id
3664
- resource_id = aws_api_gateway_resource.proxy_resource.id
4470
+ resource_id = each.value
3665
4471
  http_method = "OPTIONS"
3666
4472
  authorization = "NONE"
3667
4473
  }
3668
4474
 
3669
- # CORS integration for OPTIONS method
4475
+ # CORS integration for OPTIONS methods
3670
4476
  resource "aws_api_gateway_integration" "options_integration" {
4477
+ for_each = local.resource_ids
4478
+
3671
4479
  rest_api_id = module.rest_api.api_id
3672
- resource_id = aws_api_gateway_resource.proxy_resource.id
3673
- 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
3674
4482
 
3675
4483
  type = "MOCK"
3676
4484
  request_templates = {
@@ -3678,11 +4486,13 @@ resource "aws_api_gateway_integration" "options_integration" {
3678
4486
  }
3679
4487
  }
3680
4488
 
3681
- # OPTIONS method response
4489
+ # OPTIONS method responses
3682
4490
  resource "aws_api_gateway_method_response" "options_response" {
4491
+ for_each = local.resource_ids
4492
+
3683
4493
  rest_api_id = module.rest_api.api_id
3684
- resource_id = aws_api_gateway_resource.proxy_resource.id
3685
- 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
3686
4496
  status_code = "204"
3687
4497
 
3688
4498
  response_parameters = {
@@ -3692,18 +4502,22 @@ resource "aws_api_gateway_method_response" "options_response" {
3692
4502
  }
3693
4503
  }
3694
4504
 
3695
- # OPTIONS integration response
4505
+ # OPTIONS integration responses
3696
4506
  resource "aws_api_gateway_integration_response" "options_integration_response" {
4507
+ for_each = local.resource_ids
4508
+
3697
4509
  rest_api_id = module.rest_api.api_id
3698
- resource_id = aws_api_gateway_resource.proxy_resource.id
3699
- http_method = aws_api_gateway_method.options_method.http_method
3700
- 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
3701
4513
 
3702
4514
  response_parameters = {
3703
4515
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
3704
4516
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
3705
4517
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
3706
4518
  }
4519
+
4520
+ depends_on = [aws_api_gateway_integration.options_integration]
3707
4521
  }
3708
4522
 
3709
4523
  # API Gateway deployment
@@ -3711,13 +4525,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3711
4525
  rest_api_id = module.rest_api.api_id
3712
4526
 
3713
4527
  triggers = {
3714
- redeployment = sha1(jsonencode([
3715
- aws_api_gateway_resource.proxy_resource.id,
3716
- aws_api_gateway_method.proxy_method.id,
3717
- aws_api_gateway_integration.lambda_integration.id,
3718
- aws_api_gateway_method.options_method.id,
3719
- aws_api_gateway_integration.options_integration.id,
3720
- ]))
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
+ )))
3721
4550
  }
3722
4551
 
3723
4552
  lifecycle {
@@ -3725,7 +4554,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
3725
4554
  }
3726
4555
 
3727
4556
  depends_on = [
3728
- aws_api_gateway_method.proxy_method,
4557
+ aws_api_gateway_method.operation_methods,
3729
4558
  aws_api_gateway_integration.lambda_integration,
3730
4559
  aws_api_gateway_method.options_method,
3731
4560
  aws_api_gateway_integration.options_integration,
@@ -3864,11 +4693,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
3864
4693
 
3865
4694
  # Lambda permission for API Gateway to invoke the function
3866
4695
  resource "aws_lambda_permission" "api_gateway_invoke" {
4696
+ for_each = local.operations
4697
+
3867
4698
  statement_id = "AllowExecutionFromAPIGateway"
3868
4699
  action = "lambda:InvokeFunction"
3869
- function_name = aws_lambda_function.api_lambda.function_name
4700
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3870
4701
  principal = "apigateway.amazonaws.com"
3871
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
4702
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
3872
4703
 
3873
4704
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
3874
4705
  }
@@ -3933,83 +4764,83 @@ output "stage_id" {
3933
4764
  value = aws_api_gateway_stage.api_stage.id
3934
4765
  }
3935
4766
 
3936
- # Lambda Function Outputs
3937
- output "lambda_function_name" {
3938
- description = "Name of the Lambda function"
3939
- 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)
3940
4771
  }
3941
4772
 
3942
- output "lambda_function_arn" {
3943
- description = "ARN of the Lambda function"
3944
- 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 }
3945
4776
  }
3946
4777
 
3947
- output "lambda_invoke_arn" {
3948
- description = "Invoke ARN of the Lambda function"
3949
- 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 }
3950
4781
  }
3951
4782
 
3952
- output "lambda_qualified_arn" {
3953
- description = "Qualified ARN of the Lambda function"
3954
- 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 }
3955
4786
  }
3956
4787
 
3957
- output "lambda_version" {
3958
- description = "Version of the Lambda function"
3959
- 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 }
3960
4791
  }
3961
4792
 
3962
- output "lambda_source_code_hash" {
3963
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
3964
- 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 }
3965
4796
  }
3966
4797
 
3967
- output "lambda_source_code_size" {
3968
- description = "Size of the Lambda deployment package in bytes"
3969
- 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
3970
4801
  }
3971
4802
 
3972
- # IAM Role Outputs
3973
- output "lambda_execution_role_arn" {
3974
- description = "ARN of the Lambda execution role"
3975
- 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 }
3976
4807
  }
3977
4808
 
3978
- output "lambda_execution_role_name" {
3979
- description = "Name of the Lambda execution role"
3980
- 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 }
3981
4812
  }
3982
4813
 
3983
4814
  output "security_group_id" {
3984
- 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."
3985
4816
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
3986
4817
  }
3987
4818
 
3988
- # Integration Outputs
3989
- output "integration_id" {
3990
- description = "ID of the Lambda integration"
3991
- 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 }
3992
4823
  }
3993
4824
 
3994
- output "proxy_resource_id" {
3995
- description = "ID of the proxy resource"
3996
- 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 }
3997
4828
  }
3998
4829
 
3999
- output "proxy_method_id" {
4000
- description = "ID of the proxy method"
4001
- 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
4002
4833
  }
4003
4834
 
4004
- # CloudWatch Log Groups
4005
- output "lambda_log_group_name" {
4006
- description = "Name of the Lambda CloudWatch log group"
4007
- 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 }
4008
4839
  }
4009
4840
 
4010
- output "lambda_log_group_arn" {
4011
- description = "ARN of the Lambda CloudWatch log group"
4012
- 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 }
4013
4844
  }
4014
4845
  ",
4015
4846
  }
@@ -4137,6 +4968,235 @@ resource "random_string" "suffix" {
4137
4968
  upper = false
4138
4969
  }
4139
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
+
4140
5200
  # Resources
4141
5201
 
4142
5202
  # Create Lambda ZIP file from the bundle directory
@@ -4211,8 +5271,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
4211
5271
  description = "Allow outbound HTTPS to AWS service endpoints"
4212
5272
  }
4213
5273
 
4214
- # Lambda function
4215
5274
  resource "aws_lambda_function" "api_lambda" {
5275
+ # One lambda function per operation, each serving just that operation
5276
+ for_each = local.operations
5277
+
4216
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
4217
5279
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
4218
5280
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -4221,8 +5283,8 @@ resource "aws_lambda_function" "api_lambda" {
4221
5283
  s3_bucket = aws_s3_object.lambda_zip.bucket
4222
5284
  s3_key = aws_s3_object.lambda_zip.key
4223
5285
  s3_object_version = aws_s3_object.lambda_zip.version_id
4224
- function_name = "CustomApiHandler-\${random_string.suffix.result}"
4225
- 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
4226
5288
  handler = "index.handler"
4227
5289
  runtime = "nodejs22.x"
4228
5290
  timeout = 30
@@ -4258,7 +5320,9 @@ resource "aws_lambda_function" "api_lambda" {
4258
5320
 
4259
5321
  # IAM role for Lambda execution
4260
5322
  resource "aws_iam_role" "lambda_execution_role" {
4261
- name = "CustomApiHandler-execution-role-\${random_string.suffix.result}"
5323
+ for_each = local.operations
5324
+
5325
+ name = local.role_name[each.key]
4262
5326
 
4263
5327
  assume_role_policy = jsonencode({
4264
5328
  Version = "2012-10-17"
@@ -4278,32 +5342,38 @@ resource "aws_iam_role" "lambda_execution_role" {
4278
5342
 
4279
5343
  # Attach basic execution policy to Lambda role
4280
5344
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
5345
+ for_each = local.operations
5346
+
4281
5347
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
4282
- role = aws_iam_role.lambda_execution_role.name
5348
+ role = aws_iam_role.lambda_execution_role[each.key].name
4283
5349
  }
4284
5350
 
4285
5351
  # Attach X-Ray tracing policy to Lambda role
4286
5352
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
5353
+ for_each = local.operations
5354
+
4287
5355
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
4288
- role = aws_iam_role.lambda_execution_role.name
5356
+ role = aws_iam_role.lambda_execution_role[each.key].name
4289
5357
  }
4290
5358
 
4291
5359
  # Attach VPC access policy to Lambda role when deployed into a VPC
4292
5360
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
4293
- count = var.enable_vpc ? 1 : 0
5361
+ for_each = var.enable_vpc ? local.operations : {}
5362
+
4294
5363
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
4295
- role = aws_iam_role.lambda_execution_role.name
5364
+ role = aws_iam_role.lambda_execution_role[each.key].name
4296
5365
  }
4297
5366
 
4298
5367
  # Additional IAM policies for Lambda (if provided)
4299
5368
  resource "aws_iam_role_policy" "lambda_additional_policies" {
4300
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
4301
- name = "CustomApiHandler-additional-policies-\${random_string.suffix.result}"
4302
- 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
4303
5373
 
4304
5374
  policy = jsonencode({
4305
5375
  Version = "2012-10-17"
4306
- Statement = local.lambda_policy_statements
5376
+ Statement = local.lambda_policy_document_statements
4307
5377
  })
4308
5378
  }
4309
5379
 
@@ -4319,6 +5389,15 @@ locals {
4319
5389
  Resource = ["\${var.appconfig_application_arn}/*"]
4320
5390
  }
4321
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
+ ]
4322
5401
  }
4323
5402
 
4324
5403
  # CloudWatch Log Group for Lambda
@@ -4326,8 +5405,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
4326
5405
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
4327
5406
  #checkov:skip=CKV_AWS_338:Log retention set to forever
4328
5407
  #checkov:skip=CKV_AWS_66:Log retention set to forever
4329
- name = "/aws/lambda/CustomApiHandler-\${random_string.suffix.result}"
4330
- tags = var.tags
5408
+ for_each = local.operations
5409
+
5410
+ name = "/aws/lambda/\${local.function_name[each.key]}"
5411
+ tags = var.tags
4331
5412
  }
4332
5413
 
4333
5414
 
@@ -4424,58 +5505,62 @@ resource "aws_lambda_permission" "authorizer_invoke" {
4424
5505
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
4425
5506
  }
4426
5507
 
4427
- # Create proxy resource (captures all paths)
4428
- resource "aws_api_gateway_resource" "proxy_resource" {
4429
- rest_api_id = module.rest_api.api_id
4430
- parent_id = module.rest_api.api_root_resource_id
4431
- path_part = "{proxy+}"
4432
- }
4433
-
4434
- # Lambda integration for REST API
4435
- resource "aws_api_gateway_integration" "lambda_integration" {
4436
- rest_api_id = module.rest_api.api_id
4437
- resource_id = aws_api_gateway_resource.proxy_resource.id
4438
- http_method = aws_api_gateway_method.proxy_method.http_method
4439
-
4440
- integration_http_method = "POST"
4441
- type = "AWS_PROXY"
4442
- uri = aws_lambda_function.api_lambda.invoke_arn
4443
-
4444
- depends_on = [aws_lambda_function.api_lambda]
4445
- }
4446
-
4447
- # Method for proxy integration
4448
- resource "aws_api_gateway_method" "proxy_method" {
5508
+ # Method per operation
5509
+ resource "aws_api_gateway_method" "operation_methods" {
4449
5510
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
5511
+ for_each = local.operations
5512
+
4450
5513
  rest_api_id = module.rest_api.api_id
4451
- resource_id = aws_api_gateway_resource.proxy_resource.id
4452
- http_method = "ANY"
5514
+ resource_id = local.operation_resource_id[each.key]
5515
+ http_method = upper(each.value.method)
4453
5516
 
4454
5517
  authorization = "CUSTOM"
4455
5518
  authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
4456
5519
 
5520
+ # Path parameters must be declared on the method
4457
5521
  request_parameters = {
4458
- "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, "{")
4459
5525
  }
4460
5526
 
4461
5527
  depends_on = [aws_api_gateway_authorizer.custom_authorizer]
4462
5528
  }
4463
5529
 
4464
- # 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
4465
5546
  resource "aws_api_gateway_method" "options_method" {
4466
5547
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
4467
5548
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
5549
+ for_each = local.resource_ids
5550
+
4468
5551
  rest_api_id = module.rest_api.api_id
4469
- resource_id = aws_api_gateway_resource.proxy_resource.id
5552
+ resource_id = each.value
4470
5553
  http_method = "OPTIONS"
4471
5554
  authorization = "NONE"
4472
5555
  }
4473
5556
 
4474
- # CORS integration for OPTIONS method
5557
+ # CORS integration for OPTIONS methods
4475
5558
  resource "aws_api_gateway_integration" "options_integration" {
5559
+ for_each = local.resource_ids
5560
+
4476
5561
  rest_api_id = module.rest_api.api_id
4477
- resource_id = aws_api_gateway_resource.proxy_resource.id
4478
- 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
4479
5564
 
4480
5565
  type = "MOCK"
4481
5566
  request_templates = {
@@ -4483,11 +5568,13 @@ resource "aws_api_gateway_integration" "options_integration" {
4483
5568
  }
4484
5569
  }
4485
5570
 
4486
- # OPTIONS method response
5571
+ # OPTIONS method responses
4487
5572
  resource "aws_api_gateway_method_response" "options_response" {
5573
+ for_each = local.resource_ids
5574
+
4488
5575
  rest_api_id = module.rest_api.api_id
4489
- resource_id = aws_api_gateway_resource.proxy_resource.id
4490
- 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
4491
5578
  status_code = "204"
4492
5579
 
4493
5580
  response_parameters = {
@@ -4497,18 +5584,22 @@ resource "aws_api_gateway_method_response" "options_response" {
4497
5584
  }
4498
5585
  }
4499
5586
 
4500
- # OPTIONS integration response
5587
+ # OPTIONS integration responses
4501
5588
  resource "aws_api_gateway_integration_response" "options_integration_response" {
5589
+ for_each = local.resource_ids
5590
+
4502
5591
  rest_api_id = module.rest_api.api_id
4503
- resource_id = aws_api_gateway_resource.proxy_resource.id
4504
- http_method = aws_api_gateway_method.options_method.http_method
4505
- 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
4506
5595
 
4507
5596
  response_parameters = {
4508
5597
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
4509
5598
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
4510
5599
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
4511
5600
  }
5601
+
5602
+ depends_on = [aws_api_gateway_integration.options_integration]
4512
5603
  }
4513
5604
 
4514
5605
  # API Gateway deployment
@@ -4516,13 +5607,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4516
5607
  rest_api_id = module.rest_api.api_id
4517
5608
 
4518
5609
  triggers = {
4519
- redeployment = sha1(jsonencode([
4520
- aws_api_gateway_resource.proxy_resource.id,
4521
- aws_api_gateway_method.proxy_method.id,
4522
- aws_api_gateway_integration.lambda_integration.id,
4523
- aws_api_gateway_method.options_method.id,
4524
- aws_api_gateway_integration.options_integration.id,
4525
- ]))
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
+ )))
4526
5632
  }
4527
5633
 
4528
5634
  lifecycle {
@@ -4530,7 +5636,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
4530
5636
  }
4531
5637
 
4532
5638
  depends_on = [
4533
- aws_api_gateway_method.proxy_method,
5639
+ aws_api_gateway_method.operation_methods,
4534
5640
  aws_api_gateway_integration.lambda_integration,
4535
5641
  aws_api_gateway_method.options_method,
4536
5642
  aws_api_gateway_integration.options_integration,
@@ -4659,11 +5765,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
4659
5765
 
4660
5766
  # Lambda permission for API Gateway to invoke the function
4661
5767
  resource "aws_lambda_permission" "api_gateway_invoke" {
5768
+ for_each = local.operations
5769
+
4662
5770
  statement_id = "AllowExecutionFromAPIGateway"
4663
5771
  action = "lambda:InvokeFunction"
4664
- function_name = aws_lambda_function.api_lambda.function_name
5772
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
4665
5773
  principal = "apigateway.amazonaws.com"
4666
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
5774
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
4667
5775
 
4668
5776
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
4669
5777
  }
@@ -4728,83 +5836,83 @@ output "stage_id" {
4728
5836
  value = aws_api_gateway_stage.api_stage.id
4729
5837
  }
4730
5838
 
4731
- # Lambda Function Outputs
4732
- output "lambda_function_name" {
4733
- description = "Name of the Lambda function"
4734
- 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)
4735
5843
  }
4736
5844
 
4737
- output "lambda_function_arn" {
4738
- description = "ARN of the Lambda function"
4739
- 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 }
4740
5848
  }
4741
5849
 
4742
- output "lambda_invoke_arn" {
4743
- description = "Invoke ARN of the Lambda function"
4744
- 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 }
4745
5853
  }
4746
5854
 
4747
- output "lambda_qualified_arn" {
4748
- description = "Qualified ARN of the Lambda function"
4749
- 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 }
4750
5858
  }
4751
5859
 
4752
- output "lambda_version" {
4753
- description = "Version of the Lambda function"
4754
- 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 }
4755
5863
  }
4756
5864
 
4757
- output "lambda_source_code_hash" {
4758
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
4759
- 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 }
4760
5868
  }
4761
5869
 
4762
- output "lambda_source_code_size" {
4763
- description = "Size of the Lambda deployment package in bytes"
4764
- 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
4765
5873
  }
4766
5874
 
4767
- # IAM Role Outputs
4768
- output "lambda_execution_role_arn" {
4769
- description = "ARN of the Lambda execution role"
4770
- 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 }
4771
5879
  }
4772
5880
 
4773
- output "lambda_execution_role_name" {
4774
- description = "Name of the Lambda execution role"
4775
- 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 }
4776
5884
  }
4777
5885
 
4778
5886
  output "security_group_id" {
4779
- 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."
4780
5888
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
4781
5889
  }
4782
5890
 
4783
- # Integration Outputs
4784
- output "integration_id" {
4785
- description = "ID of the Lambda integration"
4786
- 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 }
4787
5895
  }
4788
5896
 
4789
- output "proxy_resource_id" {
4790
- description = "ID of the proxy resource"
4791
- 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 }
4792
5900
  }
4793
5901
 
4794
- output "proxy_method_id" {
4795
- description = "ID of the proxy method"
4796
- 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
4797
5905
  }
4798
5906
 
4799
- # CloudWatch Log Groups
4800
- output "lambda_log_group_name" {
4801
- description = "Name of the Lambda CloudWatch log group"
4802
- 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 }
4803
5911
  }
4804
5912
 
4805
- output "lambda_log_group_arn" {
4806
- description = "ARN of the Lambda CloudWatch log group"
4807
- 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 }
4808
5916
  }
4809
5917
  "
4810
5918
  `;