From 9f64bb4109bf1dd4145e4054eeaf4b61910aacdb Mon Sep 17 00:00:00 2001 From: Script47 Date: Mon, 5 Jan 2026 11:02:23 +0000 Subject: [PATCH] feat(lambda-function): role and policy tidy up - conditional role creation - conditional logging - conditional async invoke config creation - prefer managed policy for CloudWatch perms - allow specifying policy arns - docs for lambda function and layer --- lambda-function/README.md | 68 +++++++++++++++++++++++++++++++++++ lambda-function/cloudwatch.tf | 2 ++ lambda-function/data.tf | 7 ---- lambda-function/iam.tf | 34 +++++++++++------- lambda-function/lambda.tf | 21 ++++++----- lambda-function/locals.tf | 10 ++++++ lambda-function/outputs.tf | 4 +-- lambda-function/variables.tf | 53 +++++++++++++++------------ lambda-layer/README.md | 29 +++++++++++++++ lambda-layer/variables.tf | 12 +++---- 10 files changed, 182 insertions(+), 58 deletions(-) create mode 100644 lambda-function/README.md create mode 100644 lambda-function/locals.tf create mode 100644 lambda-layer/README.md diff --git a/lambda-function/README.md b/lambda-function/README.md new file mode 100644 index 0000000..dac8447 --- /dev/null +++ b/lambda-function/README.md @@ -0,0 +1,68 @@ +# Lambda Function + +## About + +This module allows you to setup a Lambda function. + +## Usage + +See `variables.tf` for the full argument reference. + +```hcl +module "static_site" { + source = "github.com/script47/aws-tf-modules/lambda-function" + + name = "my-lambda-func" + description = "Some description for my lambda function" + + role_arn = "my-existing-role-name" # if omitted, a role will be created by the module + policy_arns = [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + layer_arns = [ + "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:layer-name:1" + ] + + runetime = "nodejs24.x" + architectures = ["arm64"] + memory = 128 + timeout = 3 + concurrency = -1 + + vars = { + MY_ENV = "VAR" + } + + src = abspath("${path.module}/../dist") + handler = "index.handler" + + logs = { + enabled = true + app_log_level = "INFO" + system_log_level = "INFO" + retention_in_days = 30 + } + + permissions = { + apigw = { + action = "lambda:InvokeFunction" + principal = "apigateway.amazonaws.com" + source_arn = "" + } + } + + async_invoke_config = { + enabled = true + max_retries = 2 + max_event_age = 3600 + failure_destination_arn = "" + success_destination_arn = "" + } + + tags = { + Project = "my-project" + Service = "my-service" + Environment = "produdction" + } +} +``` diff --git a/lambda-function/cloudwatch.tf b/lambda-function/cloudwatch.tf index 5d5d7ee..eaef521 100644 --- a/lambda-function/cloudwatch.tf +++ b/lambda-function/cloudwatch.tf @@ -3,6 +3,8 @@ locals { } resource "aws_cloudwatch_log_group" "logs" { + count = var.logs.enabled ? 1 : 0 + name = local.log_group_name log_group_class = "STANDARD" retention_in_days = var.logs.retention_in_days diff --git a/lambda-function/data.tf b/lambda-function/data.tf index ebfab81..a1bb7ed 100644 --- a/lambda-function/data.tf +++ b/lambda-function/data.tf @@ -3,15 +3,8 @@ locals { output_path = "${local.output_dir}/${var.name}" } -resource "null_resource" "create_build_dir" { - provisioner "local-exec" { - command = "mkdir -p ${local.output_dir}" - } -} - data "archive_file" "func" { type = "zip" source_dir = var.src output_path = local.output_path - depends_on = [null_resource.create_build_dir] } diff --git a/lambda-function/iam.tf b/lambda-function/iam.tf index 8e909e3..9cac297 100644 --- a/lambda-function/iam.tf +++ b/lambda-function/iam.tf @@ -1,18 +1,26 @@ -resource "aws_iam_role_policy" "logging" { - name = "allow-cloudwatch-logs-access" - role = split("/", var.role_arn)[1] - policy = jsonencode({ - Version = "2012-10-17", +resource "aws_iam_role" "lambda" { + count = var.role_arn == null ? 1 : 0 + + name = "${var.name}-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" Statement = [ { - Effect = "Allow", - Action = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - Resource = "${aws_cloudwatch_log_group.logs.arn}:*" - } + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + }, ] }) + + tags = var.tags } + +resource "aws_iam_role_policy_attachment" "multiple" { + for_each = local.policy_arns + + role = local.role_name + policy_arn = each.value +} \ No newline at end of file diff --git a/lambda-function/lambda.tf b/lambda-function/lambda.tf index cbdba23..15a8080 100644 --- a/lambda-function/lambda.tf +++ b/lambda-function/lambda.tf @@ -1,7 +1,7 @@ resource "aws_lambda_function" "fn" { function_name = var.name description = var.description - role = var.role_arn + role = local.role_arn runtime = var.runtime architectures = var.architectures memory_size = var.memory @@ -17,11 +17,15 @@ resource "aws_lambda_function" "fn" { variables = var.vars } - logging_config { - log_group = aws_cloudwatch_log_group.logs.name - log_format = "JSON" - application_log_level = var.logs.app_log_level - system_log_level = var.logs.system_log_level + dynamic "logging_config" { + for_each = var.logs.enabled ? [1] : [] + + content { + log_group = aws_cloudwatch_log_group.logs[0].name + log_format = "JSON" + application_log_level = var.logs.app_log_level + system_log_level = var.logs.system_log_level + } } tags = var.tags @@ -30,16 +34,17 @@ resource "aws_lambda_function" "fn" { resource "aws_lambda_permission" "permissions" { for_each = var.permissions + statement_id = each.key action = each.value.action function_name = aws_lambda_function.fn.function_name principal = each.value.principal - statement_id = each.key source_arn = each.value.source_arn } resource "aws_lambda_function_event_invoke_config" "invoke_config" { - function_name = aws_lambda_function.fn.function_name + count = var.async_invoke_config.enabled ? 1 : 0 + function_name = aws_lambda_function.fn.function_name maximum_retry_attempts = var.async_invoke_config.max_retries maximum_event_age_in_seconds = var.async_invoke_config.max_event_age diff --git a/lambda-function/locals.tf b/lambda-function/locals.tf new file mode 100644 index 0000000..79e8907 --- /dev/null +++ b/lambda-function/locals.tf @@ -0,0 +1,10 @@ +locals { + role_arn = var.role_arn == null ? aws_iam_role.lambda[0].arn : var.role_arn + role_name = var.role_arn == null ? aws_iam_role.lambda[0].name : basename(var.role_arn) + policy_arns = setunion( + var.policy_arns, + var.logs.enabled ? [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] : [] + ) +} diff --git a/lambda-function/outputs.tf b/lambda-function/outputs.tf index 1a2ec98..a585987 100644 --- a/lambda-function/outputs.tf +++ b/lambda-function/outputs.tf @@ -7,6 +7,6 @@ output "lambda" { output "cloudwatch" { value = { - arn = aws_cloudwatch_log_group.logs.arn + arn = length(aws_cloudwatch_log_group.logs) > 0 ? aws_cloudwatch_log_group.logs[0].arn : null } -} \ No newline at end of file +} diff --git a/lambda-function/variables.tf b/lambda-function/variables.tf index 0377605..e31cd75 100644 --- a/lambda-function/variables.tf +++ b/lambda-function/variables.tf @@ -11,7 +11,20 @@ variable "description" { variable "role_arn" { type = string - description = "ARN of the role assumed by the function" + description = "ARN of the role assumed by the function. If unspecified a role will be created" + default = null +} + +variable "policy_arns" { + type = list(string) + description = "Option list of policy ARNs to attach to the execution role" + default = [] +} + +variable "layer_arns" { + type = list(string) + default = [] + description = "ARN of layers" } variable "runtime" { @@ -21,7 +34,7 @@ variable "runtime" { } variable "architectures" { - type = list(string) + type = set(string) default = ["arm64"] description = "A list of the supported architectures" } @@ -44,17 +57,6 @@ variable "concurrency" { description = "Set the maximum execution concurrency" } -variable "layer_arns" { - type = list(string) - default = [] - description = "ARN of layers" -} - -variable "handler" { - type = string - description = "The function's entrypoint" -} - variable "vars" { type = map(string) default = {} @@ -66,8 +68,14 @@ variable "src" { description = "The path to your function code" } +variable "handler" { + type = string + description = "The function's entrypoint" +} + variable "logs" { type = object({ + enabled = optional(bool, true) app_log_level = optional(string, "INFO") # TRACE, DEBUG, INFO, WARN, ERROR, FATAL system_log_level = optional(string, "INFO") # DEBUG, INFO, WARN retention_in_days = optional(number, 30) @@ -75,8 +83,18 @@ variable "logs" { default = {} } +variable "permissions" { + type = map(object({ + action = string + principal = string + source_arn = optional(string, null) + })) + default = {} +} + variable "async_invoke_config" { type = object({ + enabled = optional(bool, false) max_retries = optional(number, 2) max_event_age = optional(number, 3600) # 1 hour failure_destination_arn = optional(string, null) @@ -85,15 +103,6 @@ variable "async_invoke_config" { default = {} } -variable "permissions" { - type = map(object({ - action = string - principal = string - source_arn = optional(string, null) - })) - default = {} -} - variable "tags" { type = map(string) description = "The tags to apply to all resources created" diff --git a/lambda-layer/README.md b/lambda-layer/README.md new file mode 100644 index 0000000..da5a9c7 --- /dev/null +++ b/lambda-layer/README.md @@ -0,0 +1,29 @@ +# Lambda Layer + +## About + +This module allows you to setup a Lambda layer. + +## Usage + +See `variables.tf` for the full argument reference. + +```hcl +module "static_site" { + source = "github.com/script47/aws-tf-modules/lambda-layer" + + name = "my-lambda-layer" + description = "Some description for my lambda layer" + + runtimes = ["nodejs24.x"] + architectures = ["arm64"] + + src = abspath("${path.module}/../dist") + + tags = { + Project = "my-project" + Service = "my-service" + Environment = "produdction" + } +} +``` diff --git a/lambda-layer/variables.tf b/lambda-layer/variables.tf index 1541a26..29ead33 100644 --- a/lambda-layer/variables.tf +++ b/lambda-layer/variables.tf @@ -9,16 +9,16 @@ variable "description" { description = "The description of the layer" } -variable "architectures" { +variable "runtimes" { type = list(string) - default = ["arm64"] - description = "The compatible architectures" + default = ["nodejs24.x"] + description = "The compatible runtimes" } -variable "runtimes" { +variable "architectures" { type = list(string) - default = ["nodejs22.x"] - description = "The compatible runtimes" + default = ["arm64"] + description = "The compatible architectures" } variable "src" {