@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
@@ -3551,6 +3551,49 @@ resource "random_string" "suffix" {
3551
3551
  upper = false
3552
3552
  }
3553
3553
 
3554
+ locals {
3555
+ # Generated at build time from the API definition
3556
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
3557
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
3558
+
3559
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
3560
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
3561
+
3562
+ function_name = { for op in keys(local.operations) : op =>
3563
+ "\${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}"
3564
+ }
3565
+ role_name = { for op in keys(local.operations) : op =>
3566
+ "\${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}"
3567
+ }
3568
+
3569
+ operation_path = { for op, details in local.operations : op =>
3570
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
3571
+ }
3572
+ route_key = { for op, details in local.operations : op =>
3573
+ "\${upper(details.method)} \${local.operation_path[op]}"
3574
+ }
3575
+
3576
+ permission_source_suffix = { for op, details in local.operations : op =>
3577
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
3578
+ }
3579
+ }
3580
+
3581
+ resource "terraform_data" "operations_metadata" {
3582
+ # Fails the plan when the operations metadata has not been generated
3583
+ input = local.operations_file
3584
+
3585
+ lifecycle {
3586
+ precondition {
3587
+ condition = fileexists(local.operations_file)
3588
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
3589
+ }
3590
+ precondition {
3591
+ condition = length(local.operations) > 0
3592
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
3593
+ }
3594
+ }
3595
+ }
3596
+
3554
3597
  # Resources
3555
3598
 
3556
3599
  # Create Lambda ZIP file from the bundle directory
@@ -3611,9 +3654,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
3611
3654
  description = "Allow outbound HTTPS to AWS service endpoints"
3612
3655
  }
3613
3656
 
3614
- # Lambda function
3615
- # This configures a single "router" lambda to serve all requests
3616
3657
  resource "aws_lambda_function" "api_lambda" {
3658
+ # One lambda function per operation, each serving just that operation
3659
+ for_each = local.operations
3660
+
3617
3661
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
3618
3662
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
3619
3663
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -3622,8 +3666,8 @@ resource "aws_lambda_function" "api_lambda" {
3622
3666
  s3_bucket = aws_s3_object.lambda_zip.bucket
3623
3667
  s3_key = aws_s3_object.lambda_zip.key
3624
3668
  s3_object_version = aws_s3_object.lambda_zip.version_id
3625
- function_name = "TestApiHandler-\${random_string.suffix.result}"
3626
- role = aws_iam_role.lambda_execution_role.arn
3669
+ function_name = local.function_name[each.key]
3670
+ role = aws_iam_role.lambda_execution_role[each.key].arn
3627
3671
  handler = "index.handler"
3628
3672
  runtime = "nodejs22.x"
3629
3673
  timeout = 30
@@ -3659,7 +3703,9 @@ resource "aws_lambda_function" "api_lambda" {
3659
3703
 
3660
3704
  # IAM role for Lambda execution
3661
3705
  resource "aws_iam_role" "lambda_execution_role" {
3662
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
3706
+ for_each = local.operations
3707
+
3708
+ name = local.role_name[each.key]
3663
3709
 
3664
3710
  assume_role_policy = jsonencode({
3665
3711
  Version = "2012-10-17"
@@ -3679,32 +3725,38 @@ resource "aws_iam_role" "lambda_execution_role" {
3679
3725
 
3680
3726
  # Attach basic execution policy to Lambda role
3681
3727
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
3728
+ for_each = local.operations
3729
+
3682
3730
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
3683
- role = aws_iam_role.lambda_execution_role.name
3731
+ role = aws_iam_role.lambda_execution_role[each.key].name
3684
3732
  }
3685
3733
 
3686
3734
  # Attach X-Ray tracing policy to Lambda role
3687
3735
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
3736
+ for_each = local.operations
3737
+
3688
3738
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
3689
- role = aws_iam_role.lambda_execution_role.name
3739
+ role = aws_iam_role.lambda_execution_role[each.key].name
3690
3740
  }
3691
3741
 
3692
3742
  # Attach VPC access policy to Lambda role when deployed into a VPC
3693
3743
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
3694
- count = var.enable_vpc ? 1 : 0
3744
+ for_each = var.enable_vpc ? local.operations : {}
3745
+
3695
3746
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
3696
- role = aws_iam_role.lambda_execution_role.name
3747
+ role = aws_iam_role.lambda_execution_role[each.key].name
3697
3748
  }
3698
3749
 
3699
3750
  # Additional IAM policies for Lambda (if provided)
3700
3751
  resource "aws_iam_role_policy" "lambda_additional_policies" {
3701
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
3702
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
3703
- role = aws_iam_role.lambda_execution_role.id
3752
+ for_each = local.operations
3753
+
3754
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
3755
+ role = aws_iam_role.lambda_execution_role[each.key].id
3704
3756
 
3705
3757
  policy = jsonencode({
3706
3758
  Version = "2012-10-17"
3707
- Statement = local.lambda_policy_statements
3759
+ Statement = local.lambda_policy_document_statements
3708
3760
  })
3709
3761
  }
3710
3762
 
@@ -3720,6 +3772,15 @@ locals {
3720
3772
  Resource = ["\${var.appconfig_application_arn}/*"]
3721
3773
  }
3722
3774
  ] : [], var.additional_iam_policy_statements)
3775
+
3776
+ # IAM rejects an empty statement list, so fall back to a no-op deny
3777
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
3778
+ {
3779
+ Effect = "Deny"
3780
+ Action = ["appconfig:GetLatestConfiguration"]
3781
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
3782
+ }
3783
+ ]
3723
3784
  }
3724
3785
 
3725
3786
  # CloudWatch Log Group for Lambda
@@ -3727,8 +3788,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
3727
3788
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
3728
3789
  #checkov:skip=CKV_AWS_338:Log retention set to forever
3729
3790
  #checkov:skip=CKV_AWS_66:Log retention set to forever
3730
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
3731
- tags = var.tags
3791
+ for_each = local.operations
3792
+
3793
+ name = "/aws/lambda/\${local.function_name[each.key]}"
3794
+ tags = var.tags
3732
3795
  }
3733
3796
 
3734
3797
 
@@ -3747,9 +3810,11 @@ resource "aws_apigatewayv2_authorizer" "cognito_authorizer" {
3747
3810
 
3748
3811
  # Lambda integration for HTTP API
3749
3812
  resource "aws_apigatewayv2_integration" "lambda_integration" {
3813
+ for_each = local.operations
3814
+
3750
3815
  api_id = module.http_api.api_id
3751
3816
  integration_type = "AWS_PROXY"
3752
- integration_uri = aws_lambda_function.api_lambda.invoke_arn
3817
+ integration_uri = aws_lambda_function.api_lambda[each.key].invoke_arn
3753
3818
 
3754
3819
  payload_format_version = "2.0"
3755
3820
  timeout_milliseconds = 30000
@@ -3757,15 +3822,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
3757
3822
  depends_on = [aws_lambda_function.api_lambda]
3758
3823
  }
3759
3824
 
3760
- # Route for proxy integration (catches all requests)
3761
- resource "aws_apigatewayv2_route" "proxy_routes" {
3762
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
3763
- # when cors settings are configured
3764
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
3825
+ # One route per operation, each targeting that operation's integration
3826
+ resource "aws_apigatewayv2_route" "operation_routes" {
3827
+ for_each = local.operations
3765
3828
 
3766
3829
  api_id = module.http_api.api_id
3767
- route_key = "\${each.key} /{proxy+}"
3768
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
3830
+ route_key = local.route_key[each.key]
3831
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
3769
3832
 
3770
3833
  authorization_type = "JWT"
3771
3834
  authorizer_id = aws_apigatewayv2_authorizer.cognito_authorizer.id
@@ -3786,11 +3849,13 @@ module "add_url_to_runtime_config" {
3786
3849
 
3787
3850
  # Lambda permission for API Gateway to invoke the function
3788
3851
  resource "aws_lambda_permission" "api_gateway_invoke" {
3852
+ for_each = local.operations
3853
+
3789
3854
  statement_id = "AllowExecutionFromAPIGateway"
3790
3855
  action = "lambda:InvokeFunction"
3791
- function_name = aws_lambda_function.api_lambda.function_name
3856
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
3792
3857
  principal = "apigateway.amazonaws.com"
3793
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
3858
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
3794
3859
 
3795
3860
  depends_on = [module.http_api, aws_lambda_function.api_lambda]
3796
3861
  }
@@ -3833,73 +3898,73 @@ output "stage_execution_arn" {
3833
3898
  value = module.http_api.stage_execution_arn
3834
3899
  }
3835
3900
 
3836
- # Lambda Function Outputs
3837
- output "lambda_function_name" {
3838
- description = "Name of the Lambda function"
3839
- value = aws_lambda_function.api_lambda.function_name
3901
+ # Lambda Function Outputs, keyed by operation name
3902
+ output "operations" {
3903
+ description = "Names of the API operations, each with its own Lambda function"
3904
+ value = keys(local.operations)
3840
3905
  }
3841
3906
 
3842
- output "lambda_function_arn" {
3843
- description = "ARN of the Lambda function"
3844
- value = aws_lambda_function.api_lambda.arn
3907
+ output "lambda_function_names" {
3908
+ description = "Name of each operation's Lambda function, keyed by operation name"
3909
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
3845
3910
  }
3846
3911
 
3847
- output "lambda_invoke_arn" {
3848
- description = "Invoke ARN of the Lambda function"
3849
- value = aws_lambda_function.api_lambda.invoke_arn
3912
+ output "lambda_function_arns" {
3913
+ description = "ARN of each operation's Lambda function, keyed by operation name"
3914
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
3850
3915
  }
3851
3916
 
3852
- output "lambda_qualified_arn" {
3853
- description = "Qualified ARN of the Lambda function"
3854
- value = aws_lambda_function.api_lambda.qualified_arn
3917
+ output "lambda_invoke_arns" {
3918
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
3919
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
3855
3920
  }
3856
3921
 
3857
- output "lambda_version" {
3858
- description = "Version of the Lambda function"
3859
- value = aws_lambda_function.api_lambda.version
3922
+ output "lambda_qualified_arns" {
3923
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
3924
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
3860
3925
  }
3861
3926
 
3862
- output "lambda_source_code_hash" {
3863
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
3864
- value = aws_lambda_function.api_lambda.source_code_hash
3927
+ output "lambda_versions" {
3928
+ description = "Version of each operation's Lambda function, keyed by operation name"
3929
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
3865
3930
  }
3866
3931
 
3867
- output "lambda_source_code_size" {
3868
- description = "Size of the Lambda deployment package in bytes"
3869
- value = aws_lambda_function.api_lambda.source_code_size
3932
+ output "lambda_source_code_hash" {
3933
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
3934
+ value = data.archive_file.lambda_zip.output_base64sha256
3870
3935
  }
3871
3936
 
3872
- # IAM Role Outputs
3873
- output "lambda_execution_role_arn" {
3874
- description = "ARN of the Lambda execution role"
3875
- value = aws_iam_role.lambda_execution_role.arn
3937
+ # IAM Role Outputs, keyed by operation name
3938
+ output "lambda_execution_role_arns" {
3939
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
3940
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
3876
3941
  }
3877
3942
 
3878
- output "lambda_execution_role_name" {
3879
- description = "Name of the Lambda execution role"
3880
- value = aws_iam_role.lambda_execution_role.name
3943
+ output "lambda_execution_role_names" {
3944
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
3945
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
3881
3946
  }
3882
3947
 
3883
3948
  output "security_group_id" {
3884
- 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."
3949
+ 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."
3885
3950
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
3886
3951
  }
3887
3952
 
3888
- # Integration Outputs
3889
- output "integration_id" {
3890
- description = "ID of the Lambda integration"
3891
- value = aws_apigatewayv2_integration.lambda_integration.id
3953
+ # Integration Outputs, keyed by operation name
3954
+ output "integration_ids" {
3955
+ description = "ID of each operation's Lambda integration, keyed by operation name"
3956
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
3892
3957
  }
3893
3958
 
3894
- # CloudWatch Log Groups
3895
- output "lambda_log_group_name" {
3896
- description = "Name of the Lambda CloudWatch log group"
3897
- value = aws_cloudwatch_log_group.lambda_logs.name
3959
+ # CloudWatch Log Groups, keyed by operation name
3960
+ output "lambda_log_group_names" {
3961
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
3962
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
3898
3963
  }
3899
3964
 
3900
- output "lambda_log_group_arn" {
3901
- description = "ARN of the Lambda CloudWatch log group"
3902
- value = aws_cloudwatch_log_group.lambda_logs.arn
3965
+ output "lambda_log_group_arns" {
3966
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
3967
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
3903
3968
  }
3904
3969
 
3905
3970
  output "api_log_group_name" {
@@ -4291,6 +4356,49 @@ resource "random_string" "suffix" {
4291
4356
  upper = false
4292
4357
  }
4293
4358
 
4359
+ locals {
4360
+ # Generated at build time from the API definition
4361
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
4362
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
4363
+
4364
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
4365
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
4366
+
4367
+ function_name = { for op in keys(local.operations) : op =>
4368
+ "\${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}"
4369
+ }
4370
+ role_name = { for op in keys(local.operations) : op =>
4371
+ "\${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}"
4372
+ }
4373
+
4374
+ operation_path = { for op, details in local.operations : op =>
4375
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
4376
+ }
4377
+ route_key = { for op, details in local.operations : op =>
4378
+ "\${upper(details.method)} \${local.operation_path[op]}"
4379
+ }
4380
+
4381
+ permission_source_suffix = { for op, details in local.operations : op =>
4382
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
4383
+ }
4384
+ }
4385
+
4386
+ resource "terraform_data" "operations_metadata" {
4387
+ # Fails the plan when the operations metadata has not been generated
4388
+ input = local.operations_file
4389
+
4390
+ lifecycle {
4391
+ precondition {
4392
+ condition = fileexists(local.operations_file)
4393
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
4394
+ }
4395
+ precondition {
4396
+ condition = length(local.operations) > 0
4397
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
4398
+ }
4399
+ }
4400
+ }
4401
+
4294
4402
  # Resources
4295
4403
 
4296
4404
  # Create Lambda ZIP file from the bundle directory
@@ -4351,9 +4459,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
4351
4459
  description = "Allow outbound HTTPS to AWS service endpoints"
4352
4460
  }
4353
4461
 
4354
- # Lambda function
4355
- # This configures a single "router" lambda to serve all requests
4356
4462
  resource "aws_lambda_function" "api_lambda" {
4463
+ # One lambda function per operation, each serving just that operation
4464
+ for_each = local.operations
4465
+
4357
4466
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
4358
4467
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
4359
4468
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -4362,8 +4471,8 @@ resource "aws_lambda_function" "api_lambda" {
4362
4471
  s3_bucket = aws_s3_object.lambda_zip.bucket
4363
4472
  s3_key = aws_s3_object.lambda_zip.key
4364
4473
  s3_object_version = aws_s3_object.lambda_zip.version_id
4365
- function_name = "TestApiHandler-\${random_string.suffix.result}"
4366
- role = aws_iam_role.lambda_execution_role.arn
4474
+ function_name = local.function_name[each.key]
4475
+ role = aws_iam_role.lambda_execution_role[each.key].arn
4367
4476
  handler = "index.handler"
4368
4477
  runtime = "nodejs22.x"
4369
4478
  timeout = 30
@@ -4399,7 +4508,9 @@ resource "aws_lambda_function" "api_lambda" {
4399
4508
 
4400
4509
  # IAM role for Lambda execution
4401
4510
  resource "aws_iam_role" "lambda_execution_role" {
4402
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
4511
+ for_each = local.operations
4512
+
4513
+ name = local.role_name[each.key]
4403
4514
 
4404
4515
  assume_role_policy = jsonencode({
4405
4516
  Version = "2012-10-17"
@@ -4419,32 +4530,38 @@ resource "aws_iam_role" "lambda_execution_role" {
4419
4530
 
4420
4531
  # Attach basic execution policy to Lambda role
4421
4532
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
4533
+ for_each = local.operations
4534
+
4422
4535
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
4423
- role = aws_iam_role.lambda_execution_role.name
4536
+ role = aws_iam_role.lambda_execution_role[each.key].name
4424
4537
  }
4425
4538
 
4426
4539
  # Attach X-Ray tracing policy to Lambda role
4427
4540
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
4541
+ for_each = local.operations
4542
+
4428
4543
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
4429
- role = aws_iam_role.lambda_execution_role.name
4544
+ role = aws_iam_role.lambda_execution_role[each.key].name
4430
4545
  }
4431
4546
 
4432
4547
  # Attach VPC access policy to Lambda role when deployed into a VPC
4433
4548
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
4434
- count = var.enable_vpc ? 1 : 0
4549
+ for_each = var.enable_vpc ? local.operations : {}
4550
+
4435
4551
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
4436
- role = aws_iam_role.lambda_execution_role.name
4552
+ role = aws_iam_role.lambda_execution_role[each.key].name
4437
4553
  }
4438
4554
 
4439
4555
  # Additional IAM policies for Lambda (if provided)
4440
4556
  resource "aws_iam_role_policy" "lambda_additional_policies" {
4441
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
4442
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
4443
- role = aws_iam_role.lambda_execution_role.id
4557
+ for_each = local.operations
4558
+
4559
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
4560
+ role = aws_iam_role.lambda_execution_role[each.key].id
4444
4561
 
4445
4562
  policy = jsonencode({
4446
4563
  Version = "2012-10-17"
4447
- Statement = local.lambda_policy_statements
4564
+ Statement = local.lambda_policy_document_statements
4448
4565
  })
4449
4566
  }
4450
4567
 
@@ -4460,6 +4577,15 @@ locals {
4460
4577
  Resource = ["\${var.appconfig_application_arn}/*"]
4461
4578
  }
4462
4579
  ] : [], var.additional_iam_policy_statements)
4580
+
4581
+ # IAM rejects an empty statement list, so fall back to a no-op deny
4582
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
4583
+ {
4584
+ Effect = "Deny"
4585
+ Action = ["appconfig:GetLatestConfiguration"]
4586
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
4587
+ }
4588
+ ]
4463
4589
  }
4464
4590
 
4465
4591
  # CloudWatch Log Group for Lambda
@@ -4467,8 +4593,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
4467
4593
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
4468
4594
  #checkov:skip=CKV_AWS_338:Log retention set to forever
4469
4595
  #checkov:skip=CKV_AWS_66:Log retention set to forever
4470
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
4471
- tags = var.tags
4596
+ for_each = local.operations
4597
+
4598
+ name = "/aws/lambda/\${local.function_name[each.key]}"
4599
+ tags = var.tags
4472
4600
  }
4473
4601
 
4474
4602
 
@@ -4554,7 +4682,8 @@ resource "aws_apigatewayv2_authorizer" "custom_authorizer" {
4554
4682
  authorizer_type = "REQUEST"
4555
4683
  authorizer_uri = aws_lambda_function.authorizer_lambda.invoke_arn
4556
4684
  authorizer_payload_format_version = "2.0"
4557
- authorizer_result_ttl_in_seconds = 0
4685
+ authorizer_result_ttl_in_seconds = 300
4686
+ identity_sources = ["$request.header.Authorization"]
4558
4687
  enable_simple_responses = true
4559
4688
  name = "TestApiAuthorizer-\${random_string.suffix.result}"
4560
4689
  }
@@ -4569,9 +4698,11 @@ resource "aws_lambda_permission" "authorizer_invoke" {
4569
4698
 
4570
4699
  # Lambda integration for HTTP API
4571
4700
  resource "aws_apigatewayv2_integration" "lambda_integration" {
4701
+ for_each = local.operations
4702
+
4572
4703
  api_id = module.http_api.api_id
4573
4704
  integration_type = "AWS_PROXY"
4574
- integration_uri = aws_lambda_function.api_lambda.invoke_arn
4705
+ integration_uri = aws_lambda_function.api_lambda[each.key].invoke_arn
4575
4706
 
4576
4707
  payload_format_version = "2.0"
4577
4708
  timeout_milliseconds = 30000
@@ -4579,15 +4710,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
4579
4710
  depends_on = [aws_lambda_function.api_lambda]
4580
4711
  }
4581
4712
 
4582
- # Route for proxy integration (catches all requests)
4583
- resource "aws_apigatewayv2_route" "proxy_routes" {
4584
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
4585
- # when cors settings are configured
4586
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
4713
+ # One route per operation, each targeting that operation's integration
4714
+ resource "aws_apigatewayv2_route" "operation_routes" {
4715
+ for_each = local.operations
4587
4716
 
4588
4717
  api_id = module.http_api.api_id
4589
- route_key = "\${each.key} /{proxy+}"
4590
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
4718
+ route_key = local.route_key[each.key]
4719
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
4591
4720
 
4592
4721
  authorization_type = "CUSTOM"
4593
4722
  authorizer_id = aws_apigatewayv2_authorizer.custom_authorizer.id
@@ -4608,11 +4737,13 @@ module "add_url_to_runtime_config" {
4608
4737
 
4609
4738
  # Lambda permission for API Gateway to invoke the function
4610
4739
  resource "aws_lambda_permission" "api_gateway_invoke" {
4740
+ for_each = local.operations
4741
+
4611
4742
  statement_id = "AllowExecutionFromAPIGateway"
4612
4743
  action = "lambda:InvokeFunction"
4613
- function_name = aws_lambda_function.api_lambda.function_name
4744
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
4614
4745
  principal = "apigateway.amazonaws.com"
4615
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
4746
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
4616
4747
 
4617
4748
  depends_on = [module.http_api, aws_lambda_function.api_lambda]
4618
4749
  }
@@ -4655,73 +4786,73 @@ output "stage_execution_arn" {
4655
4786
  value = module.http_api.stage_execution_arn
4656
4787
  }
4657
4788
 
4658
- # Lambda Function Outputs
4659
- output "lambda_function_name" {
4660
- description = "Name of the Lambda function"
4661
- value = aws_lambda_function.api_lambda.function_name
4789
+ # Lambda Function Outputs, keyed by operation name
4790
+ output "operations" {
4791
+ description = "Names of the API operations, each with its own Lambda function"
4792
+ value = keys(local.operations)
4662
4793
  }
4663
4794
 
4664
- output "lambda_function_arn" {
4665
- description = "ARN of the Lambda function"
4666
- value = aws_lambda_function.api_lambda.arn
4795
+ output "lambda_function_names" {
4796
+ description = "Name of each operation's Lambda function, keyed by operation name"
4797
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
4667
4798
  }
4668
4799
 
4669
- output "lambda_invoke_arn" {
4670
- description = "Invoke ARN of the Lambda function"
4671
- value = aws_lambda_function.api_lambda.invoke_arn
4800
+ output "lambda_function_arns" {
4801
+ description = "ARN of each operation's Lambda function, keyed by operation name"
4802
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
4672
4803
  }
4673
4804
 
4674
- output "lambda_qualified_arn" {
4675
- description = "Qualified ARN of the Lambda function"
4676
- value = aws_lambda_function.api_lambda.qualified_arn
4805
+ output "lambda_invoke_arns" {
4806
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
4807
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
4677
4808
  }
4678
4809
 
4679
- output "lambda_version" {
4680
- description = "Version of the Lambda function"
4681
- value = aws_lambda_function.api_lambda.version
4810
+ output "lambda_qualified_arns" {
4811
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
4812
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
4682
4813
  }
4683
4814
 
4684
- output "lambda_source_code_hash" {
4685
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
4686
- value = aws_lambda_function.api_lambda.source_code_hash
4815
+ output "lambda_versions" {
4816
+ description = "Version of each operation's Lambda function, keyed by operation name"
4817
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
4687
4818
  }
4688
4819
 
4689
- output "lambda_source_code_size" {
4690
- description = "Size of the Lambda deployment package in bytes"
4691
- value = aws_lambda_function.api_lambda.source_code_size
4820
+ output "lambda_source_code_hash" {
4821
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
4822
+ value = data.archive_file.lambda_zip.output_base64sha256
4692
4823
  }
4693
4824
 
4694
- # IAM Role Outputs
4695
- output "lambda_execution_role_arn" {
4696
- description = "ARN of the Lambda execution role"
4697
- value = aws_iam_role.lambda_execution_role.arn
4825
+ # IAM Role Outputs, keyed by operation name
4826
+ output "lambda_execution_role_arns" {
4827
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
4828
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
4698
4829
  }
4699
4830
 
4700
- output "lambda_execution_role_name" {
4701
- description = "Name of the Lambda execution role"
4702
- value = aws_iam_role.lambda_execution_role.name
4831
+ output "lambda_execution_role_names" {
4832
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
4833
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
4703
4834
  }
4704
4835
 
4705
4836
  output "security_group_id" {
4706
- 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."
4837
+ 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."
4707
4838
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
4708
4839
  }
4709
4840
 
4710
- # Integration Outputs
4711
- output "integration_id" {
4712
- description = "ID of the Lambda integration"
4713
- value = aws_apigatewayv2_integration.lambda_integration.id
4841
+ # Integration Outputs, keyed by operation name
4842
+ output "integration_ids" {
4843
+ description = "ID of each operation's Lambda integration, keyed by operation name"
4844
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
4714
4845
  }
4715
4846
 
4716
- # CloudWatch Log Groups
4717
- output "lambda_log_group_name" {
4718
- description = "Name of the Lambda CloudWatch log group"
4719
- value = aws_cloudwatch_log_group.lambda_logs.name
4847
+ # CloudWatch Log Groups, keyed by operation name
4848
+ output "lambda_log_group_names" {
4849
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
4850
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
4720
4851
  }
4721
4852
 
4722
- output "lambda_log_group_arn" {
4723
- description = "ARN of the Lambda CloudWatch log group"
4724
- value = aws_cloudwatch_log_group.lambda_logs.arn
4853
+ output "lambda_log_group_arns" {
4854
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
4855
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
4725
4856
  }
4726
4857
 
4727
4858
  output "api_log_group_name" {
@@ -5113,6 +5244,49 @@ resource "random_string" "suffix" {
5113
5244
  upper = false
5114
5245
  }
5115
5246
 
5247
+ locals {
5248
+ # Generated at build time from the API definition
5249
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
5250
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
5251
+
5252
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
5253
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
5254
+
5255
+ function_name = { for op in keys(local.operations) : op =>
5256
+ "\${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}"
5257
+ }
5258
+ role_name = { for op in keys(local.operations) : op =>
5259
+ "\${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}"
5260
+ }
5261
+
5262
+ operation_path = { for op, details in local.operations : op =>
5263
+ startswith(details.path, "/") ? details.path : "/\${details.path}"
5264
+ }
5265
+ route_key = { for op, details in local.operations : op =>
5266
+ "\${upper(details.method)} \${local.operation_path[op]}"
5267
+ }
5268
+
5269
+ permission_source_suffix = { for op, details in local.operations : op =>
5270
+ "\${upper(details.method)}\${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
5271
+ }
5272
+ }
5273
+
5274
+ resource "terraform_data" "operations_metadata" {
5275
+ # Fails the plan when the operations metadata has not been generated
5276
+ input = local.operations_file
5277
+
5278
+ lifecycle {
5279
+ precondition {
5280
+ condition = fileexists(local.operations_file)
5281
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
5282
+ }
5283
+ precondition {
5284
+ condition = length(local.operations) > 0
5285
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
5286
+ }
5287
+ }
5288
+ }
5289
+
5116
5290
  # Resources
5117
5291
 
5118
5292
  # Create Lambda ZIP file from the bundle directory
@@ -5173,9 +5347,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
5173
5347
  description = "Allow outbound HTTPS to AWS service endpoints"
5174
5348
  }
5175
5349
 
5176
- # Lambda function
5177
- # This configures a single "router" lambda to serve all requests
5178
5350
  resource "aws_lambda_function" "api_lambda" {
5351
+ # One lambda function per operation, each serving just that operation
5352
+ for_each = local.operations
5353
+
5179
5354
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
5180
5355
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
5181
5356
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -5184,8 +5359,8 @@ resource "aws_lambda_function" "api_lambda" {
5184
5359
  s3_bucket = aws_s3_object.lambda_zip.bucket
5185
5360
  s3_key = aws_s3_object.lambda_zip.key
5186
5361
  s3_object_version = aws_s3_object.lambda_zip.version_id
5187
- function_name = "TestApiHandler-\${random_string.suffix.result}"
5188
- role = aws_iam_role.lambda_execution_role.arn
5362
+ function_name = local.function_name[each.key]
5363
+ role = aws_iam_role.lambda_execution_role[each.key].arn
5189
5364
  handler = "index.handler"
5190
5365
  runtime = "nodejs22.x"
5191
5366
  timeout = 30
@@ -5221,7 +5396,9 @@ resource "aws_lambda_function" "api_lambda" {
5221
5396
 
5222
5397
  # IAM role for Lambda execution
5223
5398
  resource "aws_iam_role" "lambda_execution_role" {
5224
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
5399
+ for_each = local.operations
5400
+
5401
+ name = local.role_name[each.key]
5225
5402
 
5226
5403
  assume_role_policy = jsonencode({
5227
5404
  Version = "2012-10-17"
@@ -5241,32 +5418,38 @@ resource "aws_iam_role" "lambda_execution_role" {
5241
5418
 
5242
5419
  # Attach basic execution policy to Lambda role
5243
5420
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
5421
+ for_each = local.operations
5422
+
5244
5423
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
5245
- role = aws_iam_role.lambda_execution_role.name
5424
+ role = aws_iam_role.lambda_execution_role[each.key].name
5246
5425
  }
5247
5426
 
5248
5427
  # Attach X-Ray tracing policy to Lambda role
5249
5428
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
5429
+ for_each = local.operations
5430
+
5250
5431
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
5251
- role = aws_iam_role.lambda_execution_role.name
5432
+ role = aws_iam_role.lambda_execution_role[each.key].name
5252
5433
  }
5253
5434
 
5254
5435
  # Attach VPC access policy to Lambda role when deployed into a VPC
5255
5436
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
5256
- count = var.enable_vpc ? 1 : 0
5437
+ for_each = var.enable_vpc ? local.operations : {}
5438
+
5257
5439
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
5258
- role = aws_iam_role.lambda_execution_role.name
5440
+ role = aws_iam_role.lambda_execution_role[each.key].name
5259
5441
  }
5260
5442
 
5261
5443
  # Additional IAM policies for Lambda (if provided)
5262
5444
  resource "aws_iam_role_policy" "lambda_additional_policies" {
5263
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
5264
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
5265
- role = aws_iam_role.lambda_execution_role.id
5445
+ for_each = local.operations
5446
+
5447
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
5448
+ role = aws_iam_role.lambda_execution_role[each.key].id
5266
5449
 
5267
5450
  policy = jsonencode({
5268
5451
  Version = "2012-10-17"
5269
- Statement = local.lambda_policy_statements
5452
+ Statement = local.lambda_policy_document_statements
5270
5453
  })
5271
5454
  }
5272
5455
 
@@ -5282,6 +5465,15 @@ locals {
5282
5465
  Resource = ["\${var.appconfig_application_arn}/*"]
5283
5466
  }
5284
5467
  ] : [], var.additional_iam_policy_statements)
5468
+
5469
+ # IAM rejects an empty statement list, so fall back to a no-op deny
5470
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
5471
+ {
5472
+ Effect = "Deny"
5473
+ Action = ["appconfig:GetLatestConfiguration"]
5474
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
5475
+ }
5476
+ ]
5285
5477
  }
5286
5478
 
5287
5479
  # CloudWatch Log Group for Lambda
@@ -5289,17 +5481,21 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
5289
5481
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
5290
5482
  #checkov:skip=CKV_AWS_338:Log retention set to forever
5291
5483
  #checkov:skip=CKV_AWS_66:Log retention set to forever
5292
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
5293
- tags = var.tags
5484
+ for_each = local.operations
5485
+
5486
+ name = "/aws/lambda/\${local.function_name[each.key]}"
5487
+ tags = var.tags
5294
5488
  }
5295
5489
 
5296
5490
 
5297
5491
 
5298
5492
  # Lambda integration for HTTP API
5299
5493
  resource "aws_apigatewayv2_integration" "lambda_integration" {
5494
+ for_each = local.operations
5495
+
5300
5496
  api_id = module.http_api.api_id
5301
5497
  integration_type = "AWS_PROXY"
5302
- integration_uri = aws_lambda_function.api_lambda.invoke_arn
5498
+ integration_uri = aws_lambda_function.api_lambda[each.key].invoke_arn
5303
5499
 
5304
5500
  payload_format_version = "2.0"
5305
5501
  timeout_milliseconds = 30000
@@ -5307,15 +5503,13 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
5307
5503
  depends_on = [aws_lambda_function.api_lambda]
5308
5504
  }
5309
5505
 
5310
- # Route for proxy integration (catches all requests)
5311
- resource "aws_apigatewayv2_route" "proxy_routes" {
5312
- # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
5313
- # when cors settings are configured
5314
- for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"])
5506
+ # One route per operation, each targeting that operation's integration
5507
+ resource "aws_apigatewayv2_route" "operation_routes" {
5508
+ for_each = local.operations
5315
5509
 
5316
5510
  api_id = module.http_api.api_id
5317
- route_key = "\${each.key} /{proxy+}"
5318
- target = "integrations/\${aws_apigatewayv2_integration.lambda_integration.id}"
5511
+ route_key = local.route_key[each.key]
5512
+ target = "integrations/\${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
5319
5513
 
5320
5514
  authorization_type = "AWS_IAM"
5321
5515
 
@@ -5335,11 +5529,13 @@ module "add_url_to_runtime_config" {
5335
5529
 
5336
5530
  # Lambda permission for API Gateway to invoke the function
5337
5531
  resource "aws_lambda_permission" "api_gateway_invoke" {
5532
+ for_each = local.operations
5533
+
5338
5534
  statement_id = "AllowExecutionFromAPIGateway"
5339
5535
  action = "lambda:InvokeFunction"
5340
- function_name = aws_lambda_function.api_lambda.function_name
5536
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
5341
5537
  principal = "apigateway.amazonaws.com"
5342
- source_arn = "\${module.http_api.api_execution_arn}/*/*"
5538
+ source_arn = "\${module.http_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
5343
5539
 
5344
5540
  depends_on = [module.http_api, aws_lambda_function.api_lambda]
5345
5541
  }
@@ -5382,73 +5578,73 @@ output "stage_execution_arn" {
5382
5578
  value = module.http_api.stage_execution_arn
5383
5579
  }
5384
5580
 
5385
- # Lambda Function Outputs
5386
- output "lambda_function_name" {
5387
- description = "Name of the Lambda function"
5388
- value = aws_lambda_function.api_lambda.function_name
5581
+ # Lambda Function Outputs, keyed by operation name
5582
+ output "operations" {
5583
+ description = "Names of the API operations, each with its own Lambda function"
5584
+ value = keys(local.operations)
5389
5585
  }
5390
5586
 
5391
- output "lambda_function_arn" {
5392
- description = "ARN of the Lambda function"
5393
- value = aws_lambda_function.api_lambda.arn
5587
+ output "lambda_function_names" {
5588
+ description = "Name of each operation's Lambda function, keyed by operation name"
5589
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
5394
5590
  }
5395
5591
 
5396
- output "lambda_invoke_arn" {
5397
- description = "Invoke ARN of the Lambda function"
5398
- value = aws_lambda_function.api_lambda.invoke_arn
5592
+ output "lambda_function_arns" {
5593
+ description = "ARN of each operation's Lambda function, keyed by operation name"
5594
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
5399
5595
  }
5400
5596
 
5401
- output "lambda_qualified_arn" {
5402
- description = "Qualified ARN of the Lambda function"
5403
- value = aws_lambda_function.api_lambda.qualified_arn
5597
+ output "lambda_invoke_arns" {
5598
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
5599
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
5404
5600
  }
5405
5601
 
5406
- output "lambda_version" {
5407
- description = "Version of the Lambda function"
5408
- value = aws_lambda_function.api_lambda.version
5602
+ output "lambda_qualified_arns" {
5603
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
5604
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
5409
5605
  }
5410
5606
 
5411
- output "lambda_source_code_hash" {
5412
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
5413
- value = aws_lambda_function.api_lambda.source_code_hash
5607
+ output "lambda_versions" {
5608
+ description = "Version of each operation's Lambda function, keyed by operation name"
5609
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
5414
5610
  }
5415
5611
 
5416
- output "lambda_source_code_size" {
5417
- description = "Size of the Lambda deployment package in bytes"
5418
- value = aws_lambda_function.api_lambda.source_code_size
5612
+ output "lambda_source_code_hash" {
5613
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
5614
+ value = data.archive_file.lambda_zip.output_base64sha256
5419
5615
  }
5420
5616
 
5421
- # IAM Role Outputs
5422
- output "lambda_execution_role_arn" {
5423
- description = "ARN of the Lambda execution role"
5424
- value = aws_iam_role.lambda_execution_role.arn
5617
+ # IAM Role Outputs, keyed by operation name
5618
+ output "lambda_execution_role_arns" {
5619
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
5620
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
5425
5621
  }
5426
5622
 
5427
- output "lambda_execution_role_name" {
5428
- description = "Name of the Lambda execution role"
5429
- value = aws_iam_role.lambda_execution_role.name
5623
+ output "lambda_execution_role_names" {
5624
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
5625
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
5430
5626
  }
5431
5627
 
5432
5628
  output "security_group_id" {
5433
- 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."
5629
+ 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."
5434
5630
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
5435
5631
  }
5436
5632
 
5437
- # Integration Outputs
5438
- output "integration_id" {
5439
- description = "ID of the Lambda integration"
5440
- value = aws_apigatewayv2_integration.lambda_integration.id
5633
+ # Integration Outputs, keyed by operation name
5634
+ output "integration_ids" {
5635
+ description = "ID of each operation's Lambda integration, keyed by operation name"
5636
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
5441
5637
  }
5442
5638
 
5443
- # CloudWatch Log Groups
5444
- output "lambda_log_group_name" {
5445
- description = "Name of the Lambda CloudWatch log group"
5446
- value = aws_cloudwatch_log_group.lambda_logs.name
5639
+ # CloudWatch Log Groups, keyed by operation name
5640
+ output "lambda_log_group_names" {
5641
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
5642
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
5447
5643
  }
5448
5644
 
5449
- output "lambda_log_group_arn" {
5450
- description = "ARN of the Lambda CloudWatch log group"
5451
- value = aws_cloudwatch_log_group.lambda_logs.arn
5645
+ output "lambda_log_group_arns" {
5646
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
5647
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
5452
5648
  }
5453
5649
 
5454
5650
  output "api_log_group_name" {
@@ -5908,6 +6104,235 @@ resource "random_string" "suffix" {
5908
6104
  upper = false
5909
6105
  }
5910
6106
 
6107
+ locals {
6108
+ # Generated at build time from the API definition
6109
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
6110
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
6111
+
6112
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
6113
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
6114
+
6115
+ function_name = { for op in keys(local.operations) : op =>
6116
+ "\${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}"
6117
+ }
6118
+ role_name = { for op in keys(local.operations) : op =>
6119
+ "\${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}"
6120
+ }
6121
+
6122
+ # A REST API needs a resource per path segment, so each operation's path is
6123
+ # split into the distinct path prefixes the resource tree is built from below
6124
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
6125
+
6126
+ path_nodes = merge([
6127
+ for op, segments in local.operation_segments : {
6128
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
6129
+ depth = i
6130
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
6131
+ part = segment
6132
+ }
6133
+ }
6134
+ ]...)
6135
+
6136
+ # Instances of a resource cannot reference one another, so each depth is a
6137
+ # separate resource. Deeper paths are reported by the precondition below.
6138
+ max_path_depth = 16
6139
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
6140
+ for path, node in local.path_nodes : path => node if node.depth == depth
6141
+ } }
6142
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
6143
+
6144
+ resource_ids = merge(
6145
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
6146
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
6147
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
6148
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
6149
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
6150
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
6151
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
6152
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
6153
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
6154
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
6155
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
6156
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
6157
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
6158
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
6159
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
6160
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
6161
+ )
6162
+
6163
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
6164
+ local.resource_ids[join("/", segments)]
6165
+ }
6166
+
6167
+ permission_source_suffix = { for op, details in local.operations : op =>
6168
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
6169
+ }
6170
+ }
6171
+
6172
+ resource "terraform_data" "operations_metadata" {
6173
+ # Fails the plan when the operations metadata has not been generated
6174
+ input = local.operations_file
6175
+
6176
+ lifecycle {
6177
+ precondition {
6178
+ condition = fileexists(local.operations_file)
6179
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
6180
+ }
6181
+ precondition {
6182
+ condition = length(local.operations) > 0
6183
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
6184
+ }
6185
+ precondition {
6186
+ condition = length(local.paths_too_deep) == 0
6187
+ 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."
6188
+ }
6189
+ }
6190
+ }
6191
+
6192
+ # API Gateway resources at path depth 0
6193
+ resource "aws_api_gateway_resource" "path_depth_0" {
6194
+ for_each = local.path_nodes_by_depth[0]
6195
+
6196
+ rest_api_id = module.rest_api.api_id
6197
+ parent_id = module.rest_api.api_root_resource_id
6198
+ path_part = each.value.part
6199
+ }
6200
+
6201
+ # API Gateway resources at path depth 1
6202
+ resource "aws_api_gateway_resource" "path_depth_1" {
6203
+ for_each = local.path_nodes_by_depth[1]
6204
+
6205
+ rest_api_id = module.rest_api.api_id
6206
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
6207
+ path_part = each.value.part
6208
+ }
6209
+
6210
+ # API Gateway resources at path depth 2
6211
+ resource "aws_api_gateway_resource" "path_depth_2" {
6212
+ for_each = local.path_nodes_by_depth[2]
6213
+
6214
+ rest_api_id = module.rest_api.api_id
6215
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
6216
+ path_part = each.value.part
6217
+ }
6218
+
6219
+ # API Gateway resources at path depth 3
6220
+ resource "aws_api_gateway_resource" "path_depth_3" {
6221
+ for_each = local.path_nodes_by_depth[3]
6222
+
6223
+ rest_api_id = module.rest_api.api_id
6224
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
6225
+ path_part = each.value.part
6226
+ }
6227
+
6228
+ # API Gateway resources at path depth 4
6229
+ resource "aws_api_gateway_resource" "path_depth_4" {
6230
+ for_each = local.path_nodes_by_depth[4]
6231
+
6232
+ rest_api_id = module.rest_api.api_id
6233
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
6234
+ path_part = each.value.part
6235
+ }
6236
+
6237
+ # API Gateway resources at path depth 5
6238
+ resource "aws_api_gateway_resource" "path_depth_5" {
6239
+ for_each = local.path_nodes_by_depth[5]
6240
+
6241
+ rest_api_id = module.rest_api.api_id
6242
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
6243
+ path_part = each.value.part
6244
+ }
6245
+
6246
+ # API Gateway resources at path depth 6
6247
+ resource "aws_api_gateway_resource" "path_depth_6" {
6248
+ for_each = local.path_nodes_by_depth[6]
6249
+
6250
+ rest_api_id = module.rest_api.api_id
6251
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
6252
+ path_part = each.value.part
6253
+ }
6254
+
6255
+ # API Gateway resources at path depth 7
6256
+ resource "aws_api_gateway_resource" "path_depth_7" {
6257
+ for_each = local.path_nodes_by_depth[7]
6258
+
6259
+ rest_api_id = module.rest_api.api_id
6260
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
6261
+ path_part = each.value.part
6262
+ }
6263
+
6264
+ # API Gateway resources at path depth 8
6265
+ resource "aws_api_gateway_resource" "path_depth_8" {
6266
+ for_each = local.path_nodes_by_depth[8]
6267
+
6268
+ rest_api_id = module.rest_api.api_id
6269
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
6270
+ path_part = each.value.part
6271
+ }
6272
+
6273
+ # API Gateway resources at path depth 9
6274
+ resource "aws_api_gateway_resource" "path_depth_9" {
6275
+ for_each = local.path_nodes_by_depth[9]
6276
+
6277
+ rest_api_id = module.rest_api.api_id
6278
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
6279
+ path_part = each.value.part
6280
+ }
6281
+
6282
+ # API Gateway resources at path depth 10
6283
+ resource "aws_api_gateway_resource" "path_depth_10" {
6284
+ for_each = local.path_nodes_by_depth[10]
6285
+
6286
+ rest_api_id = module.rest_api.api_id
6287
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
6288
+ path_part = each.value.part
6289
+ }
6290
+
6291
+ # API Gateway resources at path depth 11
6292
+ resource "aws_api_gateway_resource" "path_depth_11" {
6293
+ for_each = local.path_nodes_by_depth[11]
6294
+
6295
+ rest_api_id = module.rest_api.api_id
6296
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
6297
+ path_part = each.value.part
6298
+ }
6299
+
6300
+ # API Gateway resources at path depth 12
6301
+ resource "aws_api_gateway_resource" "path_depth_12" {
6302
+ for_each = local.path_nodes_by_depth[12]
6303
+
6304
+ rest_api_id = module.rest_api.api_id
6305
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
6306
+ path_part = each.value.part
6307
+ }
6308
+
6309
+ # API Gateway resources at path depth 13
6310
+ resource "aws_api_gateway_resource" "path_depth_13" {
6311
+ for_each = local.path_nodes_by_depth[13]
6312
+
6313
+ rest_api_id = module.rest_api.api_id
6314
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
6315
+ path_part = each.value.part
6316
+ }
6317
+
6318
+ # API Gateway resources at path depth 14
6319
+ resource "aws_api_gateway_resource" "path_depth_14" {
6320
+ for_each = local.path_nodes_by_depth[14]
6321
+
6322
+ rest_api_id = module.rest_api.api_id
6323
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
6324
+ path_part = each.value.part
6325
+ }
6326
+
6327
+ # API Gateway resources at path depth 15
6328
+ resource "aws_api_gateway_resource" "path_depth_15" {
6329
+ for_each = local.path_nodes_by_depth[15]
6330
+
6331
+ rest_api_id = module.rest_api.api_id
6332
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
6333
+ path_part = each.value.part
6334
+ }
6335
+
5911
6336
  # Resources
5912
6337
 
5913
6338
  # Create Lambda ZIP file from the bundle directory
@@ -5982,8 +6407,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
5982
6407
  description = "Allow outbound HTTPS to AWS service endpoints"
5983
6408
  }
5984
6409
 
5985
- # Lambda function
5986
6410
  resource "aws_lambda_function" "api_lambda" {
6411
+ # One lambda function per operation, each serving just that operation
6412
+ for_each = local.operations
6413
+
5987
6414
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
5988
6415
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
5989
6416
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -5992,8 +6419,8 @@ resource "aws_lambda_function" "api_lambda" {
5992
6419
  s3_bucket = aws_s3_object.lambda_zip.bucket
5993
6420
  s3_key = aws_s3_object.lambda_zip.key
5994
6421
  s3_object_version = aws_s3_object.lambda_zip.version_id
5995
- function_name = "TestApiHandler-\${random_string.suffix.result}"
5996
- role = aws_iam_role.lambda_execution_role.arn
6422
+ function_name = local.function_name[each.key]
6423
+ role = aws_iam_role.lambda_execution_role[each.key].arn
5997
6424
  handler = "index.handler"
5998
6425
  runtime = "nodejs22.x"
5999
6426
  timeout = 30
@@ -6029,7 +6456,9 @@ resource "aws_lambda_function" "api_lambda" {
6029
6456
 
6030
6457
  # IAM role for Lambda execution
6031
6458
  resource "aws_iam_role" "lambda_execution_role" {
6032
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
6459
+ for_each = local.operations
6460
+
6461
+ name = local.role_name[each.key]
6033
6462
 
6034
6463
  assume_role_policy = jsonencode({
6035
6464
  Version = "2012-10-17"
@@ -6049,32 +6478,38 @@ resource "aws_iam_role" "lambda_execution_role" {
6049
6478
 
6050
6479
  # Attach basic execution policy to Lambda role
6051
6480
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
6481
+ for_each = local.operations
6482
+
6052
6483
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
6053
- role = aws_iam_role.lambda_execution_role.name
6484
+ role = aws_iam_role.lambda_execution_role[each.key].name
6054
6485
  }
6055
6486
 
6056
6487
  # Attach X-Ray tracing policy to Lambda role
6057
6488
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
6489
+ for_each = local.operations
6490
+
6058
6491
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
6059
- role = aws_iam_role.lambda_execution_role.name
6492
+ role = aws_iam_role.lambda_execution_role[each.key].name
6060
6493
  }
6061
6494
 
6062
6495
  # Attach VPC access policy to Lambda role when deployed into a VPC
6063
6496
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
6064
- count = var.enable_vpc ? 1 : 0
6497
+ for_each = var.enable_vpc ? local.operations : {}
6498
+
6065
6499
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
6066
- role = aws_iam_role.lambda_execution_role.name
6500
+ role = aws_iam_role.lambda_execution_role[each.key].name
6067
6501
  }
6068
6502
 
6069
6503
  # Additional IAM policies for Lambda (if provided)
6070
6504
  resource "aws_iam_role_policy" "lambda_additional_policies" {
6071
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
6072
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
6073
- role = aws_iam_role.lambda_execution_role.id
6505
+ for_each = local.operations
6506
+
6507
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
6508
+ role = aws_iam_role.lambda_execution_role[each.key].id
6074
6509
 
6075
6510
  policy = jsonencode({
6076
6511
  Version = "2012-10-17"
6077
- Statement = local.lambda_policy_statements
6512
+ Statement = local.lambda_policy_document_statements
6078
6513
  })
6079
6514
  }
6080
6515
 
@@ -6090,6 +6525,15 @@ locals {
6090
6525
  Resource = ["\${var.appconfig_application_arn}/*"]
6091
6526
  }
6092
6527
  ] : [], var.additional_iam_policy_statements)
6528
+
6529
+ # IAM rejects an empty statement list, so fall back to a no-op deny
6530
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
6531
+ {
6532
+ Effect = "Deny"
6533
+ Action = ["appconfig:GetLatestConfiguration"]
6534
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
6535
+ }
6536
+ ]
6093
6537
  }
6094
6538
 
6095
6539
  # CloudWatch Log Group for Lambda
@@ -6097,8 +6541,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
6097
6541
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
6098
6542
  #checkov:skip=CKV_AWS_338:Log retention set to forever
6099
6543
  #checkov:skip=CKV_AWS_66:Log retention set to forever
6100
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
6101
- tags = var.tags
6544
+ for_each = local.operations
6545
+
6546
+ name = "/aws/lambda/\${local.function_name[each.key]}"
6547
+ tags = var.tags
6102
6548
  }
6103
6549
 
6104
6550
 
@@ -6111,33 +6557,14 @@ resource "aws_api_gateway_authorizer" "cognito_authorizer" {
6111
6557
  identity_source = "method.request.header.Authorization"
6112
6558
  }
6113
6559
 
6114
- # Create proxy resource (captures all paths)
6115
- resource "aws_api_gateway_resource" "proxy_resource" {
6116
- rest_api_id = module.rest_api.api_id
6117
- parent_id = module.rest_api.api_root_resource_id
6118
- path_part = "{proxy+}"
6119
- }
6120
-
6121
- # Lambda integration for REST API
6122
- resource "aws_api_gateway_integration" "lambda_integration" {
6123
- rest_api_id = module.rest_api.api_id
6124
- resource_id = aws_api_gateway_resource.proxy_resource.id
6125
- http_method = aws_api_gateway_method.proxy_method.http_method
6126
-
6127
- integration_http_method = "POST"
6128
- type = "AWS_PROXY"
6129
- uri = aws_lambda_function.api_lambda.response_streaming_invoke_arn
6130
- response_transfer_mode = "STREAM"
6131
-
6132
- depends_on = [aws_lambda_function.api_lambda]
6133
- }
6134
-
6135
- # Method for proxy integration
6136
- resource "aws_api_gateway_method" "proxy_method" {
6560
+ # Method per operation
6561
+ resource "aws_api_gateway_method" "operation_methods" {
6137
6562
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
6563
+ for_each = local.operations
6564
+
6138
6565
  rest_api_id = module.rest_api.api_id
6139
- resource_id = aws_api_gateway_resource.proxy_resource.id
6140
- http_method = "ANY"
6566
+ resource_id = local.operation_resource_id[each.key]
6567
+ http_method = upper(each.value.method)
6141
6568
 
6142
6569
  authorization = "COGNITO_USER_POOLS"
6143
6570
  authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
@@ -6145,28 +6572,51 @@ resource "aws_api_gateway_method" "proxy_method" {
6145
6572
  # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
6146
6573
  authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
6147
6574
 
6575
+ # Path parameters must be declared on the method
6148
6576
  request_parameters = {
6149
- "method.request.path.proxy" = true
6577
+ for segment in local.operation_segments[each.key] :
6578
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
6579
+ if startswith(segment, "{")
6150
6580
  }
6151
6581
 
6152
6582
  depends_on = [aws_api_gateway_authorizer.cognito_authorizer]
6153
6583
  }
6154
6584
 
6155
- # OPTIONS method for CORS preflight
6585
+ # Lambda integration per operation
6586
+ resource "aws_api_gateway_integration" "lambda_integration" {
6587
+ for_each = local.operations
6588
+
6589
+ rest_api_id = module.rest_api.api_id
6590
+ resource_id = local.operation_resource_id[each.key]
6591
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
6592
+
6593
+ integration_http_method = "POST"
6594
+ type = "AWS_PROXY"
6595
+ uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
6596
+ response_transfer_mode = "STREAM"
6597
+
6598
+ depends_on = [aws_lambda_function.api_lambda]
6599
+ }
6600
+
6601
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
6156
6602
  resource "aws_api_gateway_method" "options_method" {
6157
6603
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
6158
6604
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
6605
+ for_each = local.resource_ids
6606
+
6159
6607
  rest_api_id = module.rest_api.api_id
6160
- resource_id = aws_api_gateway_resource.proxy_resource.id
6608
+ resource_id = each.value
6161
6609
  http_method = "OPTIONS"
6162
6610
  authorization = "NONE"
6163
6611
  }
6164
6612
 
6165
- # CORS integration for OPTIONS method
6613
+ # CORS integration for OPTIONS methods
6166
6614
  resource "aws_api_gateway_integration" "options_integration" {
6615
+ for_each = local.resource_ids
6616
+
6167
6617
  rest_api_id = module.rest_api.api_id
6168
- resource_id = aws_api_gateway_resource.proxy_resource.id
6169
- http_method = aws_api_gateway_method.options_method.http_method
6618
+ resource_id = each.value
6619
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
6170
6620
 
6171
6621
  type = "MOCK"
6172
6622
  request_templates = {
@@ -6174,11 +6624,13 @@ resource "aws_api_gateway_integration" "options_integration" {
6174
6624
  }
6175
6625
  }
6176
6626
 
6177
- # OPTIONS method response
6627
+ # OPTIONS method responses
6178
6628
  resource "aws_api_gateway_method_response" "options_response" {
6629
+ for_each = local.resource_ids
6630
+
6179
6631
  rest_api_id = module.rest_api.api_id
6180
- resource_id = aws_api_gateway_resource.proxy_resource.id
6181
- http_method = aws_api_gateway_method.options_method.http_method
6632
+ resource_id = each.value
6633
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
6182
6634
  status_code = "204"
6183
6635
 
6184
6636
  response_parameters = {
@@ -6188,18 +6640,22 @@ resource "aws_api_gateway_method_response" "options_response" {
6188
6640
  }
6189
6641
  }
6190
6642
 
6191
- # OPTIONS integration response
6643
+ # OPTIONS integration responses
6192
6644
  resource "aws_api_gateway_integration_response" "options_integration_response" {
6645
+ for_each = local.resource_ids
6646
+
6193
6647
  rest_api_id = module.rest_api.api_id
6194
- resource_id = aws_api_gateway_resource.proxy_resource.id
6195
- http_method = aws_api_gateway_method.options_method.http_method
6196
- status_code = aws_api_gateway_method_response.options_response.status_code
6648
+ resource_id = each.value
6649
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
6650
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
6197
6651
 
6198
6652
  response_parameters = {
6199
6653
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
6200
6654
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
6201
6655
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
6202
6656
  }
6657
+
6658
+ depends_on = [aws_api_gateway_integration.options_integration]
6203
6659
  }
6204
6660
 
6205
6661
  # API Gateway deployment
@@ -6207,13 +6663,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
6207
6663
  rest_api_id = module.rest_api.api_id
6208
6664
 
6209
6665
  triggers = {
6210
- redeployment = sha1(jsonencode([
6211
- aws_api_gateway_resource.proxy_resource.id,
6212
- aws_api_gateway_method.proxy_method.id,
6213
- aws_api_gateway_integration.lambda_integration.id,
6214
- aws_api_gateway_method.options_method.id,
6215
- aws_api_gateway_integration.options_integration.id,
6216
- ]))
6666
+ redeployment = sha1(jsonencode(concat(
6667
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
6668
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
6669
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
6670
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
6671
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
6672
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
6673
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
6674
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
6675
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
6676
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
6677
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
6678
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
6679
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
6680
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
6681
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
6682
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
6683
+ [for m in aws_api_gateway_method.operation_methods : m.id],
6684
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
6685
+ [for m in aws_api_gateway_method.options_method : m.id],
6686
+ [for i in aws_api_gateway_integration.options_integration : i.id],
6687
+ )))
6217
6688
  }
6218
6689
 
6219
6690
  lifecycle {
@@ -6221,7 +6692,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
6221
6692
  }
6222
6693
 
6223
6694
  depends_on = [
6224
- aws_api_gateway_method.proxy_method,
6695
+ aws_api_gateway_method.operation_methods,
6225
6696
  aws_api_gateway_integration.lambda_integration,
6226
6697
  aws_api_gateway_method.options_method,
6227
6698
  aws_api_gateway_integration.options_integration,
@@ -6349,11 +6820,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
6349
6820
 
6350
6821
  # Lambda permission for API Gateway to invoke the function
6351
6822
  resource "aws_lambda_permission" "api_gateway_invoke" {
6823
+ for_each = local.operations
6824
+
6352
6825
  statement_id = "AllowExecutionFromAPIGateway"
6353
6826
  action = "lambda:InvokeFunction"
6354
- function_name = aws_lambda_function.api_lambda.function_name
6827
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
6355
6828
  principal = "apigateway.amazonaws.com"
6356
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
6829
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
6357
6830
 
6358
6831
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
6359
6832
  }
@@ -6418,83 +6891,83 @@ output "stage_id" {
6418
6891
  value = aws_api_gateway_stage.api_stage.id
6419
6892
  }
6420
6893
 
6421
- # Lambda Function Outputs
6422
- output "lambda_function_name" {
6423
- description = "Name of the Lambda function"
6424
- value = aws_lambda_function.api_lambda.function_name
6894
+ # Lambda Function Outputs, keyed by operation name
6895
+ output "operations" {
6896
+ description = "Names of the API operations, each with its own Lambda function"
6897
+ value = keys(local.operations)
6425
6898
  }
6426
6899
 
6427
- output "lambda_function_arn" {
6428
- description = "ARN of the Lambda function"
6429
- value = aws_lambda_function.api_lambda.arn
6900
+ output "lambda_function_names" {
6901
+ description = "Name of each operation's Lambda function, keyed by operation name"
6902
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
6430
6903
  }
6431
6904
 
6432
- output "lambda_invoke_arn" {
6433
- description = "Invoke ARN of the Lambda function"
6434
- value = aws_lambda_function.api_lambda.invoke_arn
6905
+ output "lambda_function_arns" {
6906
+ description = "ARN of each operation's Lambda function, keyed by operation name"
6907
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
6435
6908
  }
6436
6909
 
6437
- output "lambda_qualified_arn" {
6438
- description = "Qualified ARN of the Lambda function"
6439
- value = aws_lambda_function.api_lambda.qualified_arn
6910
+ output "lambda_invoke_arns" {
6911
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
6912
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
6440
6913
  }
6441
6914
 
6442
- output "lambda_version" {
6443
- description = "Version of the Lambda function"
6444
- value = aws_lambda_function.api_lambda.version
6915
+ output "lambda_qualified_arns" {
6916
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
6917
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
6445
6918
  }
6446
6919
 
6447
- output "lambda_source_code_hash" {
6448
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
6449
- value = aws_lambda_function.api_lambda.source_code_hash
6920
+ output "lambda_versions" {
6921
+ description = "Version of each operation's Lambda function, keyed by operation name"
6922
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
6450
6923
  }
6451
6924
 
6452
- output "lambda_source_code_size" {
6453
- description = "Size of the Lambda deployment package in bytes"
6454
- value = aws_lambda_function.api_lambda.source_code_size
6925
+ output "lambda_source_code_hash" {
6926
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
6927
+ value = data.archive_file.lambda_zip.output_base64sha256
6455
6928
  }
6456
6929
 
6457
- # IAM Role Outputs
6458
- output "lambda_execution_role_arn" {
6459
- description = "ARN of the Lambda execution role"
6460
- value = aws_iam_role.lambda_execution_role.arn
6930
+ # IAM Role Outputs, keyed by operation name
6931
+ output "lambda_execution_role_arns" {
6932
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
6933
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
6461
6934
  }
6462
6935
 
6463
- output "lambda_execution_role_name" {
6464
- description = "Name of the Lambda execution role"
6465
- value = aws_iam_role.lambda_execution_role.name
6936
+ output "lambda_execution_role_names" {
6937
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
6938
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
6466
6939
  }
6467
6940
 
6468
6941
  output "security_group_id" {
6469
- 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."
6942
+ 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."
6470
6943
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
6471
6944
  }
6472
6945
 
6473
- # Integration Outputs
6474
- output "integration_id" {
6475
- description = "ID of the Lambda integration"
6476
- value = aws_api_gateway_integration.lambda_integration.id
6946
+ # Integration Outputs, keyed by operation name
6947
+ output "integration_ids" {
6948
+ description = "ID of each operation's Lambda integration, keyed by operation name"
6949
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
6477
6950
  }
6478
6951
 
6479
- output "proxy_resource_id" {
6480
- description = "ID of the proxy resource"
6481
- value = aws_api_gateway_resource.proxy_resource.id
6952
+ output "method_ids" {
6953
+ description = "ID of each operation's API Gateway method, keyed by operation name"
6954
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
6482
6955
  }
6483
6956
 
6484
- output "proxy_method_id" {
6485
- description = "ID of the proxy method"
6486
- value = aws_api_gateway_method.proxy_method.id
6957
+ output "resource_ids" {
6958
+ description = "ID of each API Gateway resource, keyed by its path"
6959
+ value = local.resource_ids
6487
6960
  }
6488
6961
 
6489
- # CloudWatch Log Groups
6490
- output "lambda_log_group_name" {
6491
- description = "Name of the Lambda CloudWatch log group"
6492
- value = aws_cloudwatch_log_group.lambda_logs.name
6962
+ # CloudWatch Log Groups, keyed by operation name
6963
+ output "lambda_log_group_names" {
6964
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
6965
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
6493
6966
  }
6494
6967
 
6495
- output "lambda_log_group_arn" {
6496
- description = "ARN of the Lambda CloudWatch log group"
6497
- value = aws_cloudwatch_log_group.lambda_logs.arn
6968
+ output "lambda_log_group_arns" {
6969
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
6970
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
6498
6971
  }
6499
6972
  ",
6500
6973
  }
@@ -6935,6 +7408,235 @@ resource "random_string" "suffix" {
6935
7408
  upper = false
6936
7409
  }
6937
7410
 
7411
+ locals {
7412
+ # Generated at build time from the API definition
7413
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
7414
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
7415
+
7416
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
7417
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
7418
+
7419
+ function_name = { for op in keys(local.operations) : op =>
7420
+ "\${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}"
7421
+ }
7422
+ role_name = { for op in keys(local.operations) : op =>
7423
+ "\${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}"
7424
+ }
7425
+
7426
+ # A REST API needs a resource per path segment, so each operation's path is
7427
+ # split into the distinct path prefixes the resource tree is built from below
7428
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
7429
+
7430
+ path_nodes = merge([
7431
+ for op, segments in local.operation_segments : {
7432
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
7433
+ depth = i
7434
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
7435
+ part = segment
7436
+ }
7437
+ }
7438
+ ]...)
7439
+
7440
+ # Instances of a resource cannot reference one another, so each depth is a
7441
+ # separate resource. Deeper paths are reported by the precondition below.
7442
+ max_path_depth = 16
7443
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
7444
+ for path, node in local.path_nodes : path => node if node.depth == depth
7445
+ } }
7446
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
7447
+
7448
+ resource_ids = merge(
7449
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
7450
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
7451
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
7452
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
7453
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
7454
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
7455
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
7456
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
7457
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
7458
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
7459
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
7460
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
7461
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
7462
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
7463
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
7464
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
7465
+ )
7466
+
7467
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
7468
+ local.resource_ids[join("/", segments)]
7469
+ }
7470
+
7471
+ permission_source_suffix = { for op, details in local.operations : op =>
7472
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
7473
+ }
7474
+ }
7475
+
7476
+ resource "terraform_data" "operations_metadata" {
7477
+ # Fails the plan when the operations metadata has not been generated
7478
+ input = local.operations_file
7479
+
7480
+ lifecycle {
7481
+ precondition {
7482
+ condition = fileexists(local.operations_file)
7483
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
7484
+ }
7485
+ precondition {
7486
+ condition = length(local.operations) > 0
7487
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
7488
+ }
7489
+ precondition {
7490
+ condition = length(local.paths_too_deep) == 0
7491
+ 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."
7492
+ }
7493
+ }
7494
+ }
7495
+
7496
+ # API Gateway resources at path depth 0
7497
+ resource "aws_api_gateway_resource" "path_depth_0" {
7498
+ for_each = local.path_nodes_by_depth[0]
7499
+
7500
+ rest_api_id = module.rest_api.api_id
7501
+ parent_id = module.rest_api.api_root_resource_id
7502
+ path_part = each.value.part
7503
+ }
7504
+
7505
+ # API Gateway resources at path depth 1
7506
+ resource "aws_api_gateway_resource" "path_depth_1" {
7507
+ for_each = local.path_nodes_by_depth[1]
7508
+
7509
+ rest_api_id = module.rest_api.api_id
7510
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
7511
+ path_part = each.value.part
7512
+ }
7513
+
7514
+ # API Gateway resources at path depth 2
7515
+ resource "aws_api_gateway_resource" "path_depth_2" {
7516
+ for_each = local.path_nodes_by_depth[2]
7517
+
7518
+ rest_api_id = module.rest_api.api_id
7519
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
7520
+ path_part = each.value.part
7521
+ }
7522
+
7523
+ # API Gateway resources at path depth 3
7524
+ resource "aws_api_gateway_resource" "path_depth_3" {
7525
+ for_each = local.path_nodes_by_depth[3]
7526
+
7527
+ rest_api_id = module.rest_api.api_id
7528
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
7529
+ path_part = each.value.part
7530
+ }
7531
+
7532
+ # API Gateway resources at path depth 4
7533
+ resource "aws_api_gateway_resource" "path_depth_4" {
7534
+ for_each = local.path_nodes_by_depth[4]
7535
+
7536
+ rest_api_id = module.rest_api.api_id
7537
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
7538
+ path_part = each.value.part
7539
+ }
7540
+
7541
+ # API Gateway resources at path depth 5
7542
+ resource "aws_api_gateway_resource" "path_depth_5" {
7543
+ for_each = local.path_nodes_by_depth[5]
7544
+
7545
+ rest_api_id = module.rest_api.api_id
7546
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
7547
+ path_part = each.value.part
7548
+ }
7549
+
7550
+ # API Gateway resources at path depth 6
7551
+ resource "aws_api_gateway_resource" "path_depth_6" {
7552
+ for_each = local.path_nodes_by_depth[6]
7553
+
7554
+ rest_api_id = module.rest_api.api_id
7555
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
7556
+ path_part = each.value.part
7557
+ }
7558
+
7559
+ # API Gateway resources at path depth 7
7560
+ resource "aws_api_gateway_resource" "path_depth_7" {
7561
+ for_each = local.path_nodes_by_depth[7]
7562
+
7563
+ rest_api_id = module.rest_api.api_id
7564
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
7565
+ path_part = each.value.part
7566
+ }
7567
+
7568
+ # API Gateway resources at path depth 8
7569
+ resource "aws_api_gateway_resource" "path_depth_8" {
7570
+ for_each = local.path_nodes_by_depth[8]
7571
+
7572
+ rest_api_id = module.rest_api.api_id
7573
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
7574
+ path_part = each.value.part
7575
+ }
7576
+
7577
+ # API Gateway resources at path depth 9
7578
+ resource "aws_api_gateway_resource" "path_depth_9" {
7579
+ for_each = local.path_nodes_by_depth[9]
7580
+
7581
+ rest_api_id = module.rest_api.api_id
7582
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
7583
+ path_part = each.value.part
7584
+ }
7585
+
7586
+ # API Gateway resources at path depth 10
7587
+ resource "aws_api_gateway_resource" "path_depth_10" {
7588
+ for_each = local.path_nodes_by_depth[10]
7589
+
7590
+ rest_api_id = module.rest_api.api_id
7591
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
7592
+ path_part = each.value.part
7593
+ }
7594
+
7595
+ # API Gateway resources at path depth 11
7596
+ resource "aws_api_gateway_resource" "path_depth_11" {
7597
+ for_each = local.path_nodes_by_depth[11]
7598
+
7599
+ rest_api_id = module.rest_api.api_id
7600
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
7601
+ path_part = each.value.part
7602
+ }
7603
+
7604
+ # API Gateway resources at path depth 12
7605
+ resource "aws_api_gateway_resource" "path_depth_12" {
7606
+ for_each = local.path_nodes_by_depth[12]
7607
+
7608
+ rest_api_id = module.rest_api.api_id
7609
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
7610
+ path_part = each.value.part
7611
+ }
7612
+
7613
+ # API Gateway resources at path depth 13
7614
+ resource "aws_api_gateway_resource" "path_depth_13" {
7615
+ for_each = local.path_nodes_by_depth[13]
7616
+
7617
+ rest_api_id = module.rest_api.api_id
7618
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
7619
+ path_part = each.value.part
7620
+ }
7621
+
7622
+ # API Gateway resources at path depth 14
7623
+ resource "aws_api_gateway_resource" "path_depth_14" {
7624
+ for_each = local.path_nodes_by_depth[14]
7625
+
7626
+ rest_api_id = module.rest_api.api_id
7627
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
7628
+ path_part = each.value.part
7629
+ }
7630
+
7631
+ # API Gateway resources at path depth 15
7632
+ resource "aws_api_gateway_resource" "path_depth_15" {
7633
+ for_each = local.path_nodes_by_depth[15]
7634
+
7635
+ rest_api_id = module.rest_api.api_id
7636
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
7637
+ path_part = each.value.part
7638
+ }
7639
+
6938
7640
  # Resources
6939
7641
 
6940
7642
  # Create Lambda ZIP file from the bundle directory
@@ -7009,8 +7711,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
7009
7711
  description = "Allow outbound HTTPS to AWS service endpoints"
7010
7712
  }
7011
7713
 
7012
- # Lambda function
7013
7714
  resource "aws_lambda_function" "api_lambda" {
7715
+ # One lambda function per operation, each serving just that operation
7716
+ for_each = local.operations
7717
+
7014
7718
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
7015
7719
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
7016
7720
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -7019,8 +7723,8 @@ resource "aws_lambda_function" "api_lambda" {
7019
7723
  s3_bucket = aws_s3_object.lambda_zip.bucket
7020
7724
  s3_key = aws_s3_object.lambda_zip.key
7021
7725
  s3_object_version = aws_s3_object.lambda_zip.version_id
7022
- function_name = "TestApiHandler-\${random_string.suffix.result}"
7023
- role = aws_iam_role.lambda_execution_role.arn
7726
+ function_name = local.function_name[each.key]
7727
+ role = aws_iam_role.lambda_execution_role[each.key].arn
7024
7728
  handler = "index.handler"
7025
7729
  runtime = "nodejs22.x"
7026
7730
  timeout = 30
@@ -7056,7 +7760,9 @@ resource "aws_lambda_function" "api_lambda" {
7056
7760
 
7057
7761
  # IAM role for Lambda execution
7058
7762
  resource "aws_iam_role" "lambda_execution_role" {
7059
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
7763
+ for_each = local.operations
7764
+
7765
+ name = local.role_name[each.key]
7060
7766
 
7061
7767
  assume_role_policy = jsonencode({
7062
7768
  Version = "2012-10-17"
@@ -7076,32 +7782,38 @@ resource "aws_iam_role" "lambda_execution_role" {
7076
7782
 
7077
7783
  # Attach basic execution policy to Lambda role
7078
7784
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
7785
+ for_each = local.operations
7786
+
7079
7787
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
7080
- role = aws_iam_role.lambda_execution_role.name
7788
+ role = aws_iam_role.lambda_execution_role[each.key].name
7081
7789
  }
7082
7790
 
7083
7791
  # Attach X-Ray tracing policy to Lambda role
7084
7792
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
7793
+ for_each = local.operations
7794
+
7085
7795
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
7086
- role = aws_iam_role.lambda_execution_role.name
7796
+ role = aws_iam_role.lambda_execution_role[each.key].name
7087
7797
  }
7088
7798
 
7089
7799
  # Attach VPC access policy to Lambda role when deployed into a VPC
7090
7800
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
7091
- count = var.enable_vpc ? 1 : 0
7801
+ for_each = var.enable_vpc ? local.operations : {}
7802
+
7092
7803
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
7093
- role = aws_iam_role.lambda_execution_role.name
7804
+ role = aws_iam_role.lambda_execution_role[each.key].name
7094
7805
  }
7095
7806
 
7096
7807
  # Additional IAM policies for Lambda (if provided)
7097
7808
  resource "aws_iam_role_policy" "lambda_additional_policies" {
7098
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
7099
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
7100
- role = aws_iam_role.lambda_execution_role.id
7809
+ for_each = local.operations
7810
+
7811
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
7812
+ role = aws_iam_role.lambda_execution_role[each.key].id
7101
7813
 
7102
7814
  policy = jsonencode({
7103
7815
  Version = "2012-10-17"
7104
- Statement = local.lambda_policy_statements
7816
+ Statement = local.lambda_policy_document_statements
7105
7817
  })
7106
7818
  }
7107
7819
 
@@ -7117,6 +7829,15 @@ locals {
7117
7829
  Resource = ["\${var.appconfig_application_arn}/*"]
7118
7830
  }
7119
7831
  ] : [], var.additional_iam_policy_statements)
7832
+
7833
+ # IAM rejects an empty statement list, so fall back to a no-op deny
7834
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
7835
+ {
7836
+ Effect = "Deny"
7837
+ Action = ["appconfig:GetLatestConfiguration"]
7838
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
7839
+ }
7840
+ ]
7120
7841
  }
7121
7842
 
7122
7843
  # CloudWatch Log Group for Lambda
@@ -7124,64 +7845,70 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
7124
7845
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
7125
7846
  #checkov:skip=CKV_AWS_338:Log retention set to forever
7126
7847
  #checkov:skip=CKV_AWS_66:Log retention set to forever
7127
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
7128
- tags = var.tags
7129
- }
7130
-
7131
-
7848
+ for_each = local.operations
7132
7849
 
7133
- # Create proxy resource (captures all paths)
7134
- resource "aws_api_gateway_resource" "proxy_resource" {
7135
- rest_api_id = module.rest_api.api_id
7136
- parent_id = module.rest_api.api_root_resource_id
7137
- path_part = "{proxy+}"
7850
+ name = "/aws/lambda/\${local.function_name[each.key]}"
7851
+ tags = var.tags
7138
7852
  }
7139
7853
 
7140
- # Lambda integration for REST API
7141
- resource "aws_api_gateway_integration" "lambda_integration" {
7142
- rest_api_id = module.rest_api.api_id
7143
- resource_id = aws_api_gateway_resource.proxy_resource.id
7144
- http_method = aws_api_gateway_method.proxy_method.http_method
7145
7854
 
7146
- integration_http_method = "POST"
7147
- type = "AWS_PROXY"
7148
- uri = aws_lambda_function.api_lambda.response_streaming_invoke_arn
7149
- response_transfer_mode = "STREAM"
7150
-
7151
- depends_on = [aws_lambda_function.api_lambda]
7152
- }
7153
7855
 
7154
- # Method for proxy integration
7155
- resource "aws_api_gateway_method" "proxy_method" {
7856
+ # Method per operation
7857
+ resource "aws_api_gateway_method" "operation_methods" {
7156
7858
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
7859
+ for_each = local.operations
7860
+
7157
7861
  rest_api_id = module.rest_api.api_id
7158
- resource_id = aws_api_gateway_resource.proxy_resource.id
7159
- http_method = "ANY"
7862
+ resource_id = local.operation_resource_id[each.key]
7863
+ http_method = upper(each.value.method)
7160
7864
 
7161
7865
  authorization = "AWS_IAM"
7162
7866
 
7867
+ # Path parameters must be declared on the method
7163
7868
  request_parameters = {
7164
- "method.request.path.proxy" = true
7869
+ for segment in local.operation_segments[each.key] :
7870
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
7871
+ if startswith(segment, "{")
7165
7872
  }
7166
7873
 
7167
7874
  depends_on = []
7168
7875
  }
7169
7876
 
7170
- # OPTIONS method for CORS preflight
7877
+ # Lambda integration per operation
7878
+ resource "aws_api_gateway_integration" "lambda_integration" {
7879
+ for_each = local.operations
7880
+
7881
+ rest_api_id = module.rest_api.api_id
7882
+ resource_id = local.operation_resource_id[each.key]
7883
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
7884
+
7885
+ integration_http_method = "POST"
7886
+ type = "AWS_PROXY"
7887
+ uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
7888
+ response_transfer_mode = "STREAM"
7889
+
7890
+ depends_on = [aws_lambda_function.api_lambda]
7891
+ }
7892
+
7893
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
7171
7894
  resource "aws_api_gateway_method" "options_method" {
7172
7895
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
7173
7896
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
7897
+ for_each = local.resource_ids
7898
+
7174
7899
  rest_api_id = module.rest_api.api_id
7175
- resource_id = aws_api_gateway_resource.proxy_resource.id
7900
+ resource_id = each.value
7176
7901
  http_method = "OPTIONS"
7177
7902
  authorization = "NONE"
7178
7903
  }
7179
7904
 
7180
- # CORS integration for OPTIONS method
7905
+ # CORS integration for OPTIONS methods
7181
7906
  resource "aws_api_gateway_integration" "options_integration" {
7907
+ for_each = local.resource_ids
7908
+
7182
7909
  rest_api_id = module.rest_api.api_id
7183
- resource_id = aws_api_gateway_resource.proxy_resource.id
7184
- http_method = aws_api_gateway_method.options_method.http_method
7910
+ resource_id = each.value
7911
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
7185
7912
 
7186
7913
  type = "MOCK"
7187
7914
  request_templates = {
@@ -7189,11 +7916,13 @@ resource "aws_api_gateway_integration" "options_integration" {
7189
7916
  }
7190
7917
  }
7191
7918
 
7192
- # OPTIONS method response
7919
+ # OPTIONS method responses
7193
7920
  resource "aws_api_gateway_method_response" "options_response" {
7921
+ for_each = local.resource_ids
7922
+
7194
7923
  rest_api_id = module.rest_api.api_id
7195
- resource_id = aws_api_gateway_resource.proxy_resource.id
7196
- http_method = aws_api_gateway_method.options_method.http_method
7924
+ resource_id = each.value
7925
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
7197
7926
  status_code = "204"
7198
7927
 
7199
7928
  response_parameters = {
@@ -7203,18 +7932,22 @@ resource "aws_api_gateway_method_response" "options_response" {
7203
7932
  }
7204
7933
  }
7205
7934
 
7206
- # OPTIONS integration response
7935
+ # OPTIONS integration responses
7207
7936
  resource "aws_api_gateway_integration_response" "options_integration_response" {
7937
+ for_each = local.resource_ids
7938
+
7208
7939
  rest_api_id = module.rest_api.api_id
7209
- resource_id = aws_api_gateway_resource.proxy_resource.id
7210
- http_method = aws_api_gateway_method.options_method.http_method
7211
- status_code = aws_api_gateway_method_response.options_response.status_code
7940
+ resource_id = each.value
7941
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
7942
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
7212
7943
 
7213
7944
  response_parameters = {
7214
7945
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
7215
7946
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
7216
7947
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
7217
7948
  }
7949
+
7950
+ depends_on = [aws_api_gateway_integration.options_integration]
7218
7951
  }
7219
7952
 
7220
7953
  # API Gateway deployment
@@ -7222,13 +7955,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
7222
7955
  rest_api_id = module.rest_api.api_id
7223
7956
 
7224
7957
  triggers = {
7225
- redeployment = sha1(jsonencode([
7226
- aws_api_gateway_resource.proxy_resource.id,
7227
- aws_api_gateway_method.proxy_method.id,
7228
- aws_api_gateway_integration.lambda_integration.id,
7229
- aws_api_gateway_method.options_method.id,
7230
- aws_api_gateway_integration.options_integration.id,
7231
- ]))
7958
+ redeployment = sha1(jsonencode(concat(
7959
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
7960
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
7961
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
7962
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
7963
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
7964
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
7965
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
7966
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
7967
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
7968
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
7969
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
7970
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
7971
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
7972
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
7973
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
7974
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
7975
+ [for m in aws_api_gateway_method.operation_methods : m.id],
7976
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
7977
+ [for m in aws_api_gateway_method.options_method : m.id],
7978
+ [for i in aws_api_gateway_integration.options_integration : i.id],
7979
+ )))
7232
7980
  }
7233
7981
 
7234
7982
  lifecycle {
@@ -7236,7 +7984,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
7236
7984
  }
7237
7985
 
7238
7986
  depends_on = [
7239
- aws_api_gateway_method.proxy_method,
7987
+ aws_api_gateway_method.operation_methods,
7240
7988
  aws_api_gateway_integration.lambda_integration,
7241
7989
  aws_api_gateway_method.options_method,
7242
7990
  aws_api_gateway_integration.options_integration,
@@ -7375,11 +8123,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
7375
8123
 
7376
8124
  # Lambda permission for API Gateway to invoke the function
7377
8125
  resource "aws_lambda_permission" "api_gateway_invoke" {
8126
+ for_each = local.operations
8127
+
7378
8128
  statement_id = "AllowExecutionFromAPIGateway"
7379
8129
  action = "lambda:InvokeFunction"
7380
- function_name = aws_lambda_function.api_lambda.function_name
8130
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
7381
8131
  principal = "apigateway.amazonaws.com"
7382
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
8132
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
7383
8133
 
7384
8134
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
7385
8135
  }
@@ -7444,83 +8194,83 @@ output "stage_id" {
7444
8194
  value = aws_api_gateway_stage.api_stage.id
7445
8195
  }
7446
8196
 
7447
- # Lambda Function Outputs
7448
- output "lambda_function_name" {
7449
- description = "Name of the Lambda function"
7450
- value = aws_lambda_function.api_lambda.function_name
8197
+ # Lambda Function Outputs, keyed by operation name
8198
+ output "operations" {
8199
+ description = "Names of the API operations, each with its own Lambda function"
8200
+ value = keys(local.operations)
7451
8201
  }
7452
8202
 
7453
- output "lambda_function_arn" {
7454
- description = "ARN of the Lambda function"
7455
- value = aws_lambda_function.api_lambda.arn
8203
+ output "lambda_function_names" {
8204
+ description = "Name of each operation's Lambda function, keyed by operation name"
8205
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
7456
8206
  }
7457
8207
 
7458
- output "lambda_invoke_arn" {
7459
- description = "Invoke ARN of the Lambda function"
7460
- value = aws_lambda_function.api_lambda.invoke_arn
8208
+ output "lambda_function_arns" {
8209
+ description = "ARN of each operation's Lambda function, keyed by operation name"
8210
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
7461
8211
  }
7462
8212
 
7463
- output "lambda_qualified_arn" {
7464
- description = "Qualified ARN of the Lambda function"
7465
- value = aws_lambda_function.api_lambda.qualified_arn
8213
+ output "lambda_invoke_arns" {
8214
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
8215
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
7466
8216
  }
7467
8217
 
7468
- output "lambda_version" {
7469
- description = "Version of the Lambda function"
7470
- value = aws_lambda_function.api_lambda.version
8218
+ output "lambda_qualified_arns" {
8219
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
8220
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
7471
8221
  }
7472
8222
 
7473
- output "lambda_source_code_hash" {
7474
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
7475
- value = aws_lambda_function.api_lambda.source_code_hash
8223
+ output "lambda_versions" {
8224
+ description = "Version of each operation's Lambda function, keyed by operation name"
8225
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
7476
8226
  }
7477
8227
 
7478
- output "lambda_source_code_size" {
7479
- description = "Size of the Lambda deployment package in bytes"
7480
- value = aws_lambda_function.api_lambda.source_code_size
8228
+ output "lambda_source_code_hash" {
8229
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
8230
+ value = data.archive_file.lambda_zip.output_base64sha256
7481
8231
  }
7482
8232
 
7483
- # IAM Role Outputs
7484
- output "lambda_execution_role_arn" {
7485
- description = "ARN of the Lambda execution role"
7486
- value = aws_iam_role.lambda_execution_role.arn
8233
+ # IAM Role Outputs, keyed by operation name
8234
+ output "lambda_execution_role_arns" {
8235
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
8236
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
7487
8237
  }
7488
8238
 
7489
- output "lambda_execution_role_name" {
7490
- description = "Name of the Lambda execution role"
7491
- value = aws_iam_role.lambda_execution_role.name
8239
+ output "lambda_execution_role_names" {
8240
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
8241
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
7492
8242
  }
7493
8243
 
7494
8244
  output "security_group_id" {
7495
- 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."
8245
+ 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."
7496
8246
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
7497
8247
  }
7498
8248
 
7499
- # Integration Outputs
7500
- output "integration_id" {
7501
- description = "ID of the Lambda integration"
7502
- value = aws_api_gateway_integration.lambda_integration.id
8249
+ # Integration Outputs, keyed by operation name
8250
+ output "integration_ids" {
8251
+ description = "ID of each operation's Lambda integration, keyed by operation name"
8252
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
7503
8253
  }
7504
8254
 
7505
- output "proxy_resource_id" {
7506
- description = "ID of the proxy resource"
7507
- value = aws_api_gateway_resource.proxy_resource.id
8255
+ output "method_ids" {
8256
+ description = "ID of each operation's API Gateway method, keyed by operation name"
8257
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
7508
8258
  }
7509
8259
 
7510
- output "proxy_method_id" {
7511
- description = "ID of the proxy method"
7512
- value = aws_api_gateway_method.proxy_method.id
8260
+ output "resource_ids" {
8261
+ description = "ID of each API Gateway resource, keyed by its path"
8262
+ value = local.resource_ids
7513
8263
  }
7514
8264
 
7515
- # CloudWatch Log Groups
7516
- output "lambda_log_group_name" {
7517
- description = "Name of the Lambda CloudWatch log group"
7518
- value = aws_cloudwatch_log_group.lambda_logs.name
8265
+ # CloudWatch Log Groups, keyed by operation name
8266
+ output "lambda_log_group_names" {
8267
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
8268
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
7519
8269
  }
7520
8270
 
7521
- output "lambda_log_group_arn" {
7522
- description = "ARN of the Lambda CloudWatch log group"
7523
- value = aws_cloudwatch_log_group.lambda_logs.arn
8271
+ output "lambda_log_group_arns" {
8272
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
8273
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
7524
8274
  }
7525
8275
  ",
7526
8276
  }
@@ -7961,6 +8711,235 @@ resource "random_string" "suffix" {
7961
8711
  upper = false
7962
8712
  }
7963
8713
 
8714
+ locals {
8715
+ # Generated at build time from the API definition
8716
+ operations_file = "\${path.module}/../../../generated/test-api/operations.json"
8717
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
8718
+
8719
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
8720
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
8721
+
8722
+ function_name = { for op in keys(local.operations) : op =>
8723
+ "\${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}"
8724
+ }
8725
+ role_name = { for op in keys(local.operations) : op =>
8726
+ "\${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}"
8727
+ }
8728
+
8729
+ # A REST API needs a resource per path segment, so each operation's path is
8730
+ # split into the distinct path prefixes the resource tree is built from below
8731
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
8732
+
8733
+ path_nodes = merge([
8734
+ for op, segments in local.operation_segments : {
8735
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
8736
+ depth = i
8737
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
8738
+ part = segment
8739
+ }
8740
+ }
8741
+ ]...)
8742
+
8743
+ # Instances of a resource cannot reference one another, so each depth is a
8744
+ # separate resource. Deeper paths are reported by the precondition below.
8745
+ max_path_depth = 16
8746
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
8747
+ for path, node in local.path_nodes : path => node if node.depth == depth
8748
+ } }
8749
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
8750
+
8751
+ resource_ids = merge(
8752
+ { for path, resource in aws_api_gateway_resource.path_depth_0 : path => resource.id },
8753
+ { for path, resource in aws_api_gateway_resource.path_depth_1 : path => resource.id },
8754
+ { for path, resource in aws_api_gateway_resource.path_depth_2 : path => resource.id },
8755
+ { for path, resource in aws_api_gateway_resource.path_depth_3 : path => resource.id },
8756
+ { for path, resource in aws_api_gateway_resource.path_depth_4 : path => resource.id },
8757
+ { for path, resource in aws_api_gateway_resource.path_depth_5 : path => resource.id },
8758
+ { for path, resource in aws_api_gateway_resource.path_depth_6 : path => resource.id },
8759
+ { for path, resource in aws_api_gateway_resource.path_depth_7 : path => resource.id },
8760
+ { for path, resource in aws_api_gateway_resource.path_depth_8 : path => resource.id },
8761
+ { for path, resource in aws_api_gateway_resource.path_depth_9 : path => resource.id },
8762
+ { for path, resource in aws_api_gateway_resource.path_depth_10 : path => resource.id },
8763
+ { for path, resource in aws_api_gateway_resource.path_depth_11 : path => resource.id },
8764
+ { for path, resource in aws_api_gateway_resource.path_depth_12 : path => resource.id },
8765
+ { for path, resource in aws_api_gateway_resource.path_depth_13 : path => resource.id },
8766
+ { for path, resource in aws_api_gateway_resource.path_depth_14 : path => resource.id },
8767
+ { for path, resource in aws_api_gateway_resource.path_depth_15 : path => resource.id },
8768
+ )
8769
+
8770
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
8771
+ local.resource_ids[join("/", segments)]
8772
+ }
8773
+
8774
+ permission_source_suffix = { for op, details in local.operations : op =>
8775
+ "\${upper(details.method)}/\${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
8776
+ }
8777
+ }
8778
+
8779
+ resource "terraform_data" "operations_metadata" {
8780
+ # Fails the plan when the operations metadata has not been generated
8781
+ input = local.operations_file
8782
+
8783
+ lifecycle {
8784
+ precondition {
8785
+ condition = fileexists(local.operations_file)
8786
+ error_message = "Operations metadata not found at \${local.operations_file}. Build the TestApi project to generate it."
8787
+ }
8788
+ precondition {
8789
+ condition = length(local.operations) > 0
8790
+ error_message = "No operations found in \${local.operations_file}. The TestApi API must define at least one operation."
8791
+ }
8792
+ precondition {
8793
+ condition = length(local.paths_too_deep) == 0
8794
+ 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."
8795
+ }
8796
+ }
8797
+ }
8798
+
8799
+ # API Gateway resources at path depth 0
8800
+ resource "aws_api_gateway_resource" "path_depth_0" {
8801
+ for_each = local.path_nodes_by_depth[0]
8802
+
8803
+ rest_api_id = module.rest_api.api_id
8804
+ parent_id = module.rest_api.api_root_resource_id
8805
+ path_part = each.value.part
8806
+ }
8807
+
8808
+ # API Gateway resources at path depth 1
8809
+ resource "aws_api_gateway_resource" "path_depth_1" {
8810
+ for_each = local.path_nodes_by_depth[1]
8811
+
8812
+ rest_api_id = module.rest_api.api_id
8813
+ parent_id = aws_api_gateway_resource.path_depth_0[each.value.parent].id
8814
+ path_part = each.value.part
8815
+ }
8816
+
8817
+ # API Gateway resources at path depth 2
8818
+ resource "aws_api_gateway_resource" "path_depth_2" {
8819
+ for_each = local.path_nodes_by_depth[2]
8820
+
8821
+ rest_api_id = module.rest_api.api_id
8822
+ parent_id = aws_api_gateway_resource.path_depth_1[each.value.parent].id
8823
+ path_part = each.value.part
8824
+ }
8825
+
8826
+ # API Gateway resources at path depth 3
8827
+ resource "aws_api_gateway_resource" "path_depth_3" {
8828
+ for_each = local.path_nodes_by_depth[3]
8829
+
8830
+ rest_api_id = module.rest_api.api_id
8831
+ parent_id = aws_api_gateway_resource.path_depth_2[each.value.parent].id
8832
+ path_part = each.value.part
8833
+ }
8834
+
8835
+ # API Gateway resources at path depth 4
8836
+ resource "aws_api_gateway_resource" "path_depth_4" {
8837
+ for_each = local.path_nodes_by_depth[4]
8838
+
8839
+ rest_api_id = module.rest_api.api_id
8840
+ parent_id = aws_api_gateway_resource.path_depth_3[each.value.parent].id
8841
+ path_part = each.value.part
8842
+ }
8843
+
8844
+ # API Gateway resources at path depth 5
8845
+ resource "aws_api_gateway_resource" "path_depth_5" {
8846
+ for_each = local.path_nodes_by_depth[5]
8847
+
8848
+ rest_api_id = module.rest_api.api_id
8849
+ parent_id = aws_api_gateway_resource.path_depth_4[each.value.parent].id
8850
+ path_part = each.value.part
8851
+ }
8852
+
8853
+ # API Gateway resources at path depth 6
8854
+ resource "aws_api_gateway_resource" "path_depth_6" {
8855
+ for_each = local.path_nodes_by_depth[6]
8856
+
8857
+ rest_api_id = module.rest_api.api_id
8858
+ parent_id = aws_api_gateway_resource.path_depth_5[each.value.parent].id
8859
+ path_part = each.value.part
8860
+ }
8861
+
8862
+ # API Gateway resources at path depth 7
8863
+ resource "aws_api_gateway_resource" "path_depth_7" {
8864
+ for_each = local.path_nodes_by_depth[7]
8865
+
8866
+ rest_api_id = module.rest_api.api_id
8867
+ parent_id = aws_api_gateway_resource.path_depth_6[each.value.parent].id
8868
+ path_part = each.value.part
8869
+ }
8870
+
8871
+ # API Gateway resources at path depth 8
8872
+ resource "aws_api_gateway_resource" "path_depth_8" {
8873
+ for_each = local.path_nodes_by_depth[8]
8874
+
8875
+ rest_api_id = module.rest_api.api_id
8876
+ parent_id = aws_api_gateway_resource.path_depth_7[each.value.parent].id
8877
+ path_part = each.value.part
8878
+ }
8879
+
8880
+ # API Gateway resources at path depth 9
8881
+ resource "aws_api_gateway_resource" "path_depth_9" {
8882
+ for_each = local.path_nodes_by_depth[9]
8883
+
8884
+ rest_api_id = module.rest_api.api_id
8885
+ parent_id = aws_api_gateway_resource.path_depth_8[each.value.parent].id
8886
+ path_part = each.value.part
8887
+ }
8888
+
8889
+ # API Gateway resources at path depth 10
8890
+ resource "aws_api_gateway_resource" "path_depth_10" {
8891
+ for_each = local.path_nodes_by_depth[10]
8892
+
8893
+ rest_api_id = module.rest_api.api_id
8894
+ parent_id = aws_api_gateway_resource.path_depth_9[each.value.parent].id
8895
+ path_part = each.value.part
8896
+ }
8897
+
8898
+ # API Gateway resources at path depth 11
8899
+ resource "aws_api_gateway_resource" "path_depth_11" {
8900
+ for_each = local.path_nodes_by_depth[11]
8901
+
8902
+ rest_api_id = module.rest_api.api_id
8903
+ parent_id = aws_api_gateway_resource.path_depth_10[each.value.parent].id
8904
+ path_part = each.value.part
8905
+ }
8906
+
8907
+ # API Gateway resources at path depth 12
8908
+ resource "aws_api_gateway_resource" "path_depth_12" {
8909
+ for_each = local.path_nodes_by_depth[12]
8910
+
8911
+ rest_api_id = module.rest_api.api_id
8912
+ parent_id = aws_api_gateway_resource.path_depth_11[each.value.parent].id
8913
+ path_part = each.value.part
8914
+ }
8915
+
8916
+ # API Gateway resources at path depth 13
8917
+ resource "aws_api_gateway_resource" "path_depth_13" {
8918
+ for_each = local.path_nodes_by_depth[13]
8919
+
8920
+ rest_api_id = module.rest_api.api_id
8921
+ parent_id = aws_api_gateway_resource.path_depth_12[each.value.parent].id
8922
+ path_part = each.value.part
8923
+ }
8924
+
8925
+ # API Gateway resources at path depth 14
8926
+ resource "aws_api_gateway_resource" "path_depth_14" {
8927
+ for_each = local.path_nodes_by_depth[14]
8928
+
8929
+ rest_api_id = module.rest_api.api_id
8930
+ parent_id = aws_api_gateway_resource.path_depth_13[each.value.parent].id
8931
+ path_part = each.value.part
8932
+ }
8933
+
8934
+ # API Gateway resources at path depth 15
8935
+ resource "aws_api_gateway_resource" "path_depth_15" {
8936
+ for_each = local.path_nodes_by_depth[15]
8937
+
8938
+ rest_api_id = module.rest_api.api_id
8939
+ parent_id = aws_api_gateway_resource.path_depth_14[each.value.parent].id
8940
+ path_part = each.value.part
8941
+ }
8942
+
7964
8943
  # Resources
7965
8944
 
7966
8945
  # Create Lambda ZIP file from the bundle directory
@@ -8035,8 +9014,10 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
8035
9014
  description = "Allow outbound HTTPS to AWS service endpoints"
8036
9015
  }
8037
9016
 
8038
- # Lambda function
8039
9017
  resource "aws_lambda_function" "api_lambda" {
9018
+ # One lambda function per operation, each serving just that operation
9019
+ for_each = local.operations
9020
+
8040
9021
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
8041
9022
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
8042
9023
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -8045,8 +9026,8 @@ resource "aws_lambda_function" "api_lambda" {
8045
9026
  s3_bucket = aws_s3_object.lambda_zip.bucket
8046
9027
  s3_key = aws_s3_object.lambda_zip.key
8047
9028
  s3_object_version = aws_s3_object.lambda_zip.version_id
8048
- function_name = "TestApiHandler-\${random_string.suffix.result}"
8049
- role = aws_iam_role.lambda_execution_role.arn
9029
+ function_name = local.function_name[each.key]
9030
+ role = aws_iam_role.lambda_execution_role[each.key].arn
8050
9031
  handler = "index.handler"
8051
9032
  runtime = "nodejs22.x"
8052
9033
  timeout = 30
@@ -8082,7 +9063,9 @@ resource "aws_lambda_function" "api_lambda" {
8082
9063
 
8083
9064
  # IAM role for Lambda execution
8084
9065
  resource "aws_iam_role" "lambda_execution_role" {
8085
- name = "TestApiHandler-execution-role-\${random_string.suffix.result}"
9066
+ for_each = local.operations
9067
+
9068
+ name = local.role_name[each.key]
8086
9069
 
8087
9070
  assume_role_policy = jsonencode({
8088
9071
  Version = "2012-10-17"
@@ -8102,32 +9085,38 @@ resource "aws_iam_role" "lambda_execution_role" {
8102
9085
 
8103
9086
  # Attach basic execution policy to Lambda role
8104
9087
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
9088
+ for_each = local.operations
9089
+
8105
9090
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
8106
- role = aws_iam_role.lambda_execution_role.name
9091
+ role = aws_iam_role.lambda_execution_role[each.key].name
8107
9092
  }
8108
9093
 
8109
9094
  # Attach X-Ray tracing policy to Lambda role
8110
9095
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
9096
+ for_each = local.operations
9097
+
8111
9098
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
8112
- role = aws_iam_role.lambda_execution_role.name
9099
+ role = aws_iam_role.lambda_execution_role[each.key].name
8113
9100
  }
8114
9101
 
8115
9102
  # Attach VPC access policy to Lambda role when deployed into a VPC
8116
9103
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
8117
- count = var.enable_vpc ? 1 : 0
9104
+ for_each = var.enable_vpc ? local.operations : {}
9105
+
8118
9106
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
8119
- role = aws_iam_role.lambda_execution_role.name
9107
+ role = aws_iam_role.lambda_execution_role[each.key].name
8120
9108
  }
8121
9109
 
8122
9110
  # Additional IAM policies for Lambda (if provided)
8123
9111
  resource "aws_iam_role_policy" "lambda_additional_policies" {
8124
- count = length(local.lambda_policy_statements) > 0 ? 1 : 0
8125
- name = "TestApiHandler-additional-policies-\${random_string.suffix.result}"
8126
- role = aws_iam_role.lambda_execution_role.id
9112
+ for_each = local.operations
9113
+
9114
+ name = "\${substr(local.function_name[each.key], 0, 55)}-policies"
9115
+ role = aws_iam_role.lambda_execution_role[each.key].id
8127
9116
 
8128
9117
  policy = jsonencode({
8129
9118
  Version = "2012-10-17"
8130
- Statement = local.lambda_policy_statements
9119
+ Statement = local.lambda_policy_document_statements
8131
9120
  })
8132
9121
  }
8133
9122
 
@@ -8143,6 +9132,15 @@ locals {
8143
9132
  Resource = ["\${var.appconfig_application_arn}/*"]
8144
9133
  }
8145
9134
  ] : [], var.additional_iam_policy_statements)
9135
+
9136
+ # IAM rejects an empty statement list, so fall back to a no-op deny
9137
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
9138
+ {
9139
+ Effect = "Deny"
9140
+ Action = ["appconfig:GetLatestConfiguration"]
9141
+ Resource = ["arn:aws:appconfig:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:application/none"]
9142
+ }
9143
+ ]
8146
9144
  }
8147
9145
 
8148
9146
  # CloudWatch Log Group for Lambda
@@ -8150,8 +9148,10 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
8150
9148
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
8151
9149
  #checkov:skip=CKV_AWS_338:Log retention set to forever
8152
9150
  #checkov:skip=CKV_AWS_66:Log retention set to forever
8153
- name = "/aws/lambda/TestApiHandler-\${random_string.suffix.result}"
8154
- tags = var.tags
9151
+ for_each = local.operations
9152
+
9153
+ name = "/aws/lambda/\${local.function_name[each.key]}"
9154
+ tags = var.tags
8155
9155
  }
8156
9156
 
8157
9157
 
@@ -8248,59 +9248,63 @@ resource "aws_lambda_permission" "authorizer_invoke" {
8248
9248
  source_arn = "\${module.rest_api.api_execution_arn}/authorizers/\${aws_api_gateway_authorizer.custom_authorizer.id}"
8249
9249
  }
8250
9250
 
8251
- # Create proxy resource (captures all paths)
8252
- resource "aws_api_gateway_resource" "proxy_resource" {
8253
- rest_api_id = module.rest_api.api_id
8254
- parent_id = module.rest_api.api_root_resource_id
8255
- path_part = "{proxy+}"
8256
- }
8257
-
8258
- # Lambda integration for REST API
8259
- resource "aws_api_gateway_integration" "lambda_integration" {
8260
- rest_api_id = module.rest_api.api_id
8261
- resource_id = aws_api_gateway_resource.proxy_resource.id
8262
- http_method = aws_api_gateway_method.proxy_method.http_method
8263
-
8264
- integration_http_method = "POST"
8265
- type = "AWS_PROXY"
8266
- uri = aws_lambda_function.api_lambda.response_streaming_invoke_arn
8267
- response_transfer_mode = "STREAM"
8268
-
8269
- depends_on = [aws_lambda_function.api_lambda]
8270
- }
8271
-
8272
- # Method for proxy integration
8273
- resource "aws_api_gateway_method" "proxy_method" {
9251
+ # Method per operation
9252
+ resource "aws_api_gateway_method" "operation_methods" {
8274
9253
  #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
9254
+ for_each = local.operations
9255
+
8275
9256
  rest_api_id = module.rest_api.api_id
8276
- resource_id = aws_api_gateway_resource.proxy_resource.id
8277
- http_method = "ANY"
9257
+ resource_id = local.operation_resource_id[each.key]
9258
+ http_method = upper(each.value.method)
8278
9259
 
8279
9260
  authorization = "CUSTOM"
8280
9261
  authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
8281
9262
 
9263
+ # Path parameters must be declared on the method
8282
9264
  request_parameters = {
8283
- "method.request.path.proxy" = true
9265
+ for segment in local.operation_segments[each.key] :
9266
+ "method.request.path.\${trimsuffix(trimprefix(segment, "{"), "}")}" => true
9267
+ if startswith(segment, "{")
8284
9268
  }
8285
9269
 
8286
9270
  depends_on = [aws_api_gateway_authorizer.custom_authorizer]
8287
9271
  }
8288
9272
 
8289
- # OPTIONS method for CORS preflight
9273
+ # Lambda integration per operation
9274
+ resource "aws_api_gateway_integration" "lambda_integration" {
9275
+ for_each = local.operations
9276
+
9277
+ rest_api_id = module.rest_api.api_id
9278
+ resource_id = local.operation_resource_id[each.key]
9279
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
9280
+
9281
+ integration_http_method = "POST"
9282
+ type = "AWS_PROXY"
9283
+ uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
9284
+ response_transfer_mode = "STREAM"
9285
+
9286
+ depends_on = [aws_lambda_function.api_lambda]
9287
+ }
9288
+
9289
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
8290
9290
  resource "aws_api_gateway_method" "options_method" {
8291
9291
  #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
8292
9292
  #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
9293
+ for_each = local.resource_ids
9294
+
8293
9295
  rest_api_id = module.rest_api.api_id
8294
- resource_id = aws_api_gateway_resource.proxy_resource.id
9296
+ resource_id = each.value
8295
9297
  http_method = "OPTIONS"
8296
9298
  authorization = "NONE"
8297
9299
  }
8298
9300
 
8299
- # CORS integration for OPTIONS method
9301
+ # CORS integration for OPTIONS methods
8300
9302
  resource "aws_api_gateway_integration" "options_integration" {
9303
+ for_each = local.resource_ids
9304
+
8301
9305
  rest_api_id = module.rest_api.api_id
8302
- resource_id = aws_api_gateway_resource.proxy_resource.id
8303
- http_method = aws_api_gateway_method.options_method.http_method
9306
+ resource_id = each.value
9307
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
8304
9308
 
8305
9309
  type = "MOCK"
8306
9310
  request_templates = {
@@ -8308,11 +9312,13 @@ resource "aws_api_gateway_integration" "options_integration" {
8308
9312
  }
8309
9313
  }
8310
9314
 
8311
- # OPTIONS method response
9315
+ # OPTIONS method responses
8312
9316
  resource "aws_api_gateway_method_response" "options_response" {
9317
+ for_each = local.resource_ids
9318
+
8313
9319
  rest_api_id = module.rest_api.api_id
8314
- resource_id = aws_api_gateway_resource.proxy_resource.id
8315
- http_method = aws_api_gateway_method.options_method.http_method
9320
+ resource_id = each.value
9321
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
8316
9322
  status_code = "204"
8317
9323
 
8318
9324
  response_parameters = {
@@ -8322,18 +9328,22 @@ resource "aws_api_gateway_method_response" "options_response" {
8322
9328
  }
8323
9329
  }
8324
9330
 
8325
- # OPTIONS integration response
9331
+ # OPTIONS integration responses
8326
9332
  resource "aws_api_gateway_integration_response" "options_integration_response" {
9333
+ for_each = local.resource_ids
9334
+
8327
9335
  rest_api_id = module.rest_api.api_id
8328
- resource_id = aws_api_gateway_resource.proxy_resource.id
8329
- http_method = aws_api_gateway_method.options_method.http_method
8330
- status_code = aws_api_gateway_method_response.options_response.status_code
9336
+ resource_id = each.value
9337
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
9338
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
8331
9339
 
8332
9340
  response_parameters = {
8333
9341
  "method.response.header.Access-Control-Allow-Headers" = "'\${join(",", var.cors_allow_headers)}'"
8334
9342
  "method.response.header.Access-Control-Allow-Methods" = "'\${join(",", var.cors_allow_methods)}'"
8335
9343
  "method.response.header.Access-Control-Allow-Origin" = "'\${join(",", var.cors_allow_origins)}'"
8336
9344
  }
9345
+
9346
+ depends_on = [aws_api_gateway_integration.options_integration]
8337
9347
  }
8338
9348
 
8339
9349
  # API Gateway deployment
@@ -8341,13 +9351,28 @@ resource "aws_api_gateway_deployment" "api_deployment" {
8341
9351
  rest_api_id = module.rest_api.api_id
8342
9352
 
8343
9353
  triggers = {
8344
- redeployment = sha1(jsonencode([
8345
- aws_api_gateway_resource.proxy_resource.id,
8346
- aws_api_gateway_method.proxy_method.id,
8347
- aws_api_gateway_integration.lambda_integration.id,
8348
- aws_api_gateway_method.options_method.id,
8349
- aws_api_gateway_integration.options_integration.id,
8350
- ]))
9354
+ redeployment = sha1(jsonencode(concat(
9355
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
9356
+ [for r in aws_api_gateway_resource.path_depth_1 : r.id],
9357
+ [for r in aws_api_gateway_resource.path_depth_2 : r.id],
9358
+ [for r in aws_api_gateway_resource.path_depth_3 : r.id],
9359
+ [for r in aws_api_gateway_resource.path_depth_4 : r.id],
9360
+ [for r in aws_api_gateway_resource.path_depth_5 : r.id],
9361
+ [for r in aws_api_gateway_resource.path_depth_6 : r.id],
9362
+ [for r in aws_api_gateway_resource.path_depth_7 : r.id],
9363
+ [for r in aws_api_gateway_resource.path_depth_8 : r.id],
9364
+ [for r in aws_api_gateway_resource.path_depth_9 : r.id],
9365
+ [for r in aws_api_gateway_resource.path_depth_10 : r.id],
9366
+ [for r in aws_api_gateway_resource.path_depth_11 : r.id],
9367
+ [for r in aws_api_gateway_resource.path_depth_12 : r.id],
9368
+ [for r in aws_api_gateway_resource.path_depth_13 : r.id],
9369
+ [for r in aws_api_gateway_resource.path_depth_14 : r.id],
9370
+ [for r in aws_api_gateway_resource.path_depth_15 : r.id],
9371
+ [for m in aws_api_gateway_method.operation_methods : m.id],
9372
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
9373
+ [for m in aws_api_gateway_method.options_method : m.id],
9374
+ [for i in aws_api_gateway_integration.options_integration : i.id],
9375
+ )))
8351
9376
  }
8352
9377
 
8353
9378
  lifecycle {
@@ -8355,7 +9380,7 @@ resource "aws_api_gateway_deployment" "api_deployment" {
8355
9380
  }
8356
9381
 
8357
9382
  depends_on = [
8358
- aws_api_gateway_method.proxy_method,
9383
+ aws_api_gateway_method.operation_methods,
8359
9384
  aws_api_gateway_integration.lambda_integration,
8360
9385
  aws_api_gateway_method.options_method,
8361
9386
  aws_api_gateway_integration.options_integration,
@@ -8484,11 +9509,13 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
8484
9509
 
8485
9510
  # Lambda permission for API Gateway to invoke the function
8486
9511
  resource "aws_lambda_permission" "api_gateway_invoke" {
9512
+ for_each = local.operations
9513
+
8487
9514
  statement_id = "AllowExecutionFromAPIGateway"
8488
9515
  action = "lambda:InvokeFunction"
8489
- function_name = aws_lambda_function.api_lambda.function_name
9516
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
8490
9517
  principal = "apigateway.amazonaws.com"
8491
- source_arn = "\${module.rest_api.api_execution_arn}/*/*"
9518
+ source_arn = "\${module.rest_api.api_execution_arn}/*/\${local.permission_source_suffix[each.key]}"
8492
9519
 
8493
9520
  depends_on = [module.rest_api, aws_lambda_function.api_lambda]
8494
9521
  }
@@ -8553,83 +9580,83 @@ output "stage_id" {
8553
9580
  value = aws_api_gateway_stage.api_stage.id
8554
9581
  }
8555
9582
 
8556
- # Lambda Function Outputs
8557
- output "lambda_function_name" {
8558
- description = "Name of the Lambda function"
8559
- value = aws_lambda_function.api_lambda.function_name
9583
+ # Lambda Function Outputs, keyed by operation name
9584
+ output "operations" {
9585
+ description = "Names of the API operations, each with its own Lambda function"
9586
+ value = keys(local.operations)
8560
9587
  }
8561
9588
 
8562
- output "lambda_function_arn" {
8563
- description = "ARN of the Lambda function"
8564
- value = aws_lambda_function.api_lambda.arn
9589
+ output "lambda_function_names" {
9590
+ description = "Name of each operation's Lambda function, keyed by operation name"
9591
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
8565
9592
  }
8566
9593
 
8567
- output "lambda_invoke_arn" {
8568
- description = "Invoke ARN of the Lambda function"
8569
- value = aws_lambda_function.api_lambda.invoke_arn
9594
+ output "lambda_function_arns" {
9595
+ description = "ARN of each operation's Lambda function, keyed by operation name"
9596
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
8570
9597
  }
8571
9598
 
8572
- output "lambda_qualified_arn" {
8573
- description = "Qualified ARN of the Lambda function"
8574
- value = aws_lambda_function.api_lambda.qualified_arn
9599
+ output "lambda_invoke_arns" {
9600
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
9601
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
8575
9602
  }
8576
9603
 
8577
- output "lambda_version" {
8578
- description = "Version of the Lambda function"
8579
- value = aws_lambda_function.api_lambda.version
9604
+ output "lambda_qualified_arns" {
9605
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
9606
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
8580
9607
  }
8581
9608
 
8582
- output "lambda_source_code_hash" {
8583
- description = "Base64-encoded SHA256 hash of the Lambda deployment package"
8584
- value = aws_lambda_function.api_lambda.source_code_hash
9609
+ output "lambda_versions" {
9610
+ description = "Version of each operation's Lambda function, keyed by operation name"
9611
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
8585
9612
  }
8586
9613
 
8587
- output "lambda_source_code_size" {
8588
- description = "Size of the Lambda deployment package in bytes"
8589
- value = aws_lambda_function.api_lambda.source_code_size
9614
+ output "lambda_source_code_hash" {
9615
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
9616
+ value = data.archive_file.lambda_zip.output_base64sha256
8590
9617
  }
8591
9618
 
8592
- # IAM Role Outputs
8593
- output "lambda_execution_role_arn" {
8594
- description = "ARN of the Lambda execution role"
8595
- value = aws_iam_role.lambda_execution_role.arn
9619
+ # IAM Role Outputs, keyed by operation name
9620
+ output "lambda_execution_role_arns" {
9621
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
9622
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
8596
9623
  }
8597
9624
 
8598
- output "lambda_execution_role_name" {
8599
- description = "Name of the Lambda execution role"
8600
- value = aws_iam_role.lambda_execution_role.name
9625
+ output "lambda_execution_role_names" {
9626
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
9627
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
8601
9628
  }
8602
9629
 
8603
9630
  output "security_group_id" {
8604
- 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."
9631
+ 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."
8605
9632
  value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
8606
9633
  }
8607
9634
 
8608
- # Integration Outputs
8609
- output "integration_id" {
8610
- description = "ID of the Lambda integration"
8611
- value = aws_api_gateway_integration.lambda_integration.id
9635
+ # Integration Outputs, keyed by operation name
9636
+ output "integration_ids" {
9637
+ description = "ID of each operation's Lambda integration, keyed by operation name"
9638
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
8612
9639
  }
8613
9640
 
8614
- output "proxy_resource_id" {
8615
- description = "ID of the proxy resource"
8616
- value = aws_api_gateway_resource.proxy_resource.id
9641
+ output "method_ids" {
9642
+ description = "ID of each operation's API Gateway method, keyed by operation name"
9643
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
8617
9644
  }
8618
9645
 
8619
- output "proxy_method_id" {
8620
- description = "ID of the proxy method"
8621
- value = aws_api_gateway_method.proxy_method.id
9646
+ output "resource_ids" {
9647
+ description = "ID of each API Gateway resource, keyed by its path"
9648
+ value = local.resource_ids
8622
9649
  }
8623
9650
 
8624
- # CloudWatch Log Groups
8625
- output "lambda_log_group_name" {
8626
- description = "Name of the Lambda CloudWatch log group"
8627
- value = aws_cloudwatch_log_group.lambda_logs.name
9651
+ # CloudWatch Log Groups, keyed by operation name
9652
+ output "lambda_log_group_names" {
9653
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
9654
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
8628
9655
  }
8629
9656
 
8630
- output "lambda_log_group_arn" {
8631
- description = "ARN of the Lambda CloudWatch log group"
8632
- value = aws_cloudwatch_log_group.lambda_logs.arn
9657
+ output "lambda_log_group_arns" {
9658
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
9659
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
8633
9660
  }
8634
9661
  ",
8635
9662
  }