Skip to content
Draft
2 changes: 1 addition & 1 deletion .github/workflows/deploy-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
permissions:
pull-requests: write
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
contents: read # This is required for actions/checkout

jobs:
validate_inputs:
Expand Down
8 changes: 4 additions & 4 deletions infrastructure/lambda-bulk-upload.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ module "bulk-upload-lambda" {
APIM_API_URL = data.aws_ssm_parameter.apim_url.value
}

is_gateway_integration_needed = false
is_invoked_from_gateway = false
lambda_timeout = 900
reserved_concurrent_executions = local.bulk_upload_lambda_concurrent_limit
is_gateway_integration_needed = false
is_invoked_from_gateway = false
lambda_timeout = 900
manage_reserved_concurrency = false

depends_on = [
module.ndr-bulk-staging-store,
Expand Down
66 changes: 64 additions & 2 deletions infrastructure/modules/lambda/main.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# Migrate existing resources to indexed form
moved {
from = aws_lambda_function.lambda
to = aws_lambda_function.lambda[0]
}

# Lambda with Terraform-managed concurrency (default - keeps existing resource name)
resource "aws_lambda_function" "lambda" {
count = var.manage_reserved_concurrency ? 1 : 0

# If the file is not in the current working directory you will need to include a
# path.module in the filename.
filename = data.archive_file.lambda.output_path
Expand Down Expand Up @@ -41,6 +50,59 @@ resource "aws_lambda_function" "lambda" {
]
}

# Lambda with externally-managed concurrency (opt-in with new resource name)
resource "aws_lambda_function" "lambda_unmanaged" {
count = var.manage_reserved_concurrency ? 0 : 1

# If the file is not in the current working directory you will need to include a
# path.module in the filename.
filename = data.archive_file.lambda.output_path
function_name = "${terraform.workspace}_${var.name}"
role = aws_iam_role.lambda_execution_role.arn
handler = var.handler
source_code_hash = data.archive_file.lambda.output_base64sha256
runtime = "python3.11"
timeout = var.lambda_timeout
memory_size = var.memory_size
reserved_concurrent_executions = var.reserved_concurrent_executions
kms_key_arn = aws_kms_key.lambda.arn

ephemeral_storage {
size = var.lambda_ephemeral_storage
}

environment {
variables = var.lambda_environment_variables
}

vpc_config {
subnet_ids = var.vpc_subnet_ids
security_group_ids = var.vpc_security_group_ids
}

layers = local.lambda_layers

lifecycle {
ignore_changes = [
# These are required as Lambdas are deployed via the CI/CD pipelines
source_code_hash,
layers,
# Allow external management of concurrency
reserved_concurrent_executions
]
}

depends_on = [
aws_iam_role_policy_attachment.default_policies,
aws_iam_role_policy_attachment.lambda_execution_policy
]
}

# Local value to reference the active lambda resource
locals {
lambda = var.manage_reserved_concurrency ? aws_lambda_function.lambda[0] : aws_lambda_function.lambda_unmanaged[0]
}

resource "aws_cloudwatch_log_group" "lambda_logs" {
count = contains(var.persistent_workspaces, terraform.workspace) ? 0 : 1
name = "/aws/lambda/${terraform.workspace}_${var.name}"
Expand Down Expand Up @@ -128,14 +190,14 @@ resource "aws_api_gateway_integration" "lambda_integration" {
http_method = each.value
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.lambda.invoke_arn
uri = local.lambda.invoke_arn
}

resource "aws_lambda_permission" "lambda_permission" {
count = var.is_invoked_from_gateway ? 1 : 0
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.lambda.arn
function_name = local.lambda.arn
principal = "apigateway.amazonaws.com"
# The "/*/*" portion grants access from any method on any resource
# within the API Gateway REST API.
Expand Down
12 changes: 6 additions & 6 deletions infrastructure/modules/lambda/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
output "invoke_arn" {
value = aws_lambda_function.lambda.invoke_arn
value = local.lambda.invoke_arn
}

output "qualified_arn" {
value = aws_lambda_function.lambda.qualified_arn
value = local.lambda.qualified_arn
}

output "function_name" {
value = aws_lambda_function.lambda.function_name
value = local.lambda.function_name
}

output "timeout" {
value = aws_lambda_function.lambda.timeout
value = local.lambda.timeout
}

output "lambda_arn" {
value = aws_lambda_function.lambda.arn
value = local.lambda.arn
}

output "lambda_execution_role_name" {
Expand All @@ -24,4 +24,4 @@ output "lambda_execution_role_name" {

output "lambda_execution_role_arn" {
value = aws_iam_role.lambda_execution_role.arn
}
}
12 changes: 12 additions & 0 deletions infrastructure/modules/lambda/variable.tf
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ variable "reserved_concurrent_executions" {
default = -1
}

variable "manage_reserved_concurrency" {
description = <<-EOT
Whether Terraform should manage reserved_concurrent_executions.
- true (default): Terraform enforces concurrency value on every deploy. Any external changes will be reset.
- false: Terraform ignores concurrency after creation, allowing external management (EventBridge, controller lambdas).

Note: When false, reserved_concurrent_executions is set initially but then ignored. External systems can freely modify it.
EOT
type = bool
default = true
}

variable "default_policies" {
description = "List of default IAM policy ARNs to attach to the Lambda execution role."
type = list(string)
Expand Down
Loading