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