Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Lambda Terraform Module

Secure-by-default AWS Lambda function — the function keystone plus its event source mappings, resource-based invoke permissions, published layer versions, an optional function URL, and aliases, from a single composite call. Container-image (ECR) deployment by default, with first-class Zip support; environment-variable CMK encryption, VPC placement, DLQ wiring, and X-Ray tracing all built in. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • ⚡ Provisions an aws_lambda_function keystone plus everything meaningless without it: event source mappings, invoke permissions, layer versions, a function URL, and aliases — one composite call, one invokable function.
  • 📦 Container-image deployment by default (package_type = "Image") — pins an immutable ECR digest and inherits terraform-aws-ecr scanning/lifecycle controls; Zip (local file or S3) is a fully-supported first-class alternative.
  • 🔑 Execution role is a required input (role) — never created here. The variable description makes the mandatory iam:PassRole grant impossible to miss (the #1 apply-time failure for Lambda).
  • 🔐 Environment variables encrypted at rest — with a customer-managed kms_key_arn when supplied, otherwise the AWS-managed service key. The CMK is wired only when variables exist, avoiding the silent-drop drift the Lambda API is known for.
  • 🌐 VPC placement via vpc_config, with the replace_security_groups_on_destroy escape hatch for the Hyperplane-ENI destroy-ordering gotcha.
  • 🔁 Poll-based event sources (SQS, Kinesis, DynamoDB Streams, MSK, self-managed Kafka, Amazon MQ, DocumentDB) as a for_each map — add or remove one trigger without perturbing the others.
  • 🚦 DLQ, tracing, reserved concurrency, SnapStart, code signing — all exposed; DLQ is wired by the presence of dead_letter_target_arn (no separate flag).
  • 🔗 Function URL disabled by default; when enabled it defaults to AWS_IAM auth — public NONE auth is a documented, deliberate opt-out only.
  • 🏷️ Universal taggingvar.tags flows to the function and its event source mappings (the only taggable resources in the schema); tags_all surfaced as an output.

💡 Why it matters: Lambda is the connective tissue of an event-driven estate. A secure-by-default function that gets iam:PassRole, environment encryption, and public-exposure decisions right from a single module keeps the serverless blast radius small — which matters more, not less, in a regulated FI.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

terraform-aws-lambda is a consumer module: it wires an execution role, an image or package, encryption, networking, and event sources from upstream foundations, and it emits an arn / invoke_arn that downstream invokers (EventBridge, S3, API Gateway) target.

flowchart LR
 iam["terraform-aws-iam-role<br/>execution role"]
 ecr["terraform-aws-ecr<br/>image_uri"]
 kms["terraform-aws-kms<br/>env-var CMK"]
 vpc["terraform-aws-vpc<br/>subnet_ids"]
 sg["terraform-aws-security-group"]
 s3["terraform-aws-s3-bucket<br/>zip package"]
 sqs["terraform-aws-sqs<br/>event source / DLQ"]
 sns["terraform-aws-sns<br/>DLQ target"]
 cwlg["terraform-aws-cloudwatch-log-group"]
 lambda["terraform-aws-lambda"]
 inv["EventBridge / S3 / API Gateway<br/>(invokers)"]

 iam -->|"role arn + iam:PassRole"| lambda
 ecr -->|"image_uri"| lambda
 kms -->|"kms_key_arn"| lambda
 vpc -->|"subnet_ids"| lambda
 sg -->|"security_group_ids"| lambda
 s3 -->|"s3_bucket / s3_key"| lambda
 sqs -->|"event_source_arn"| lambda
 sns -.->|"dead_letter_target_arn"| lambda
 cwlg -.->|"logging_config.log_group"| lambda
 lambda -->|"arn / invoke_arn"| inv

 style lambda fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading

🧬 What this module builds

flowchart TB
 subgraph LMOD["terraform-aws-lambda"]
 fn["aws_lambda_function.this<br/>(keystone)"]
 esm["aws_lambda_event_source_mapping.this<br/>for_each event_source_mappings"]
 perm["aws_lambda_permission.this<br/>for_each permissions"]
 layer["aws_lambda_layer_version.this<br/>for_each layer_versions"]
 url["aws_lambda_function_url.this<br/>optional singleton"]
 alias["aws_lambda_alias.this<br/>for_each aliases"]

 fn --> esm
 fn --> perm
 fn --> url
 fn --> alias
 layer -.->|"layer_version_arns -> var.layers"| fn
 end

 style fn fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading
Resource Role Cardinality Taggable
aws_lambda_function.this Keystone function 1
aws_lambda_event_source_mapping.this Poll-based triggers per event_source_mappings entry
aws_lambda_permission.this Resource-based invoke grants per permissions entry
aws_lambda_layer_version.this Layer versions published here per layer_versions entry
aws_lambda_function_url.this HTTPS invoke endpoint 0–1 (create_function_url)
aws_lambda_alias.this Named version pointers per aliases entry

ℹ️ Only aws_lambda_function and aws_lambda_event_source_mapping expose a tags argument in the v6 provider schema — the module deliberately does not attempt to tag permissions, layers, URLs, or aliases.


✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module — the caller's configured provider (region, credentials, default_tags) is inherited. Credentials resolve through the standard AWS chain (env → SSO/shared credentials → assume_role → instance profile / IRSA → OIDC) at the root/pipeline level; there are no credential variables.


🔑 Required IAM Permissions

Least-privilege actions the Terraform execution identity needs to create, read, update, and delete everything this module manages:

Action Required for Notes
lambda:CreateFunction, lambda:GetFunction, lambda:GetFunctionConfiguration, lambda:UpdateFunctionCode, lambda:UpdateFunctionConfiguration, lambda:DeleteFunction Function lifecycle Core CRUD
lambda:PublishVersion Immutable versions Only when publish = true
lambda:TagResource, lambda:UntagResource, lambda:ListTags Tagging Function + event source mappings
lambda:CreateEventSourceMapping, lambda:GetEventSourceMapping, lambda:UpdateEventSourceMapping, lambda:DeleteEventSourceMapping Event source mappings Only when used
lambda:AddPermission, lambda:GetPolicy, lambda:RemovePermission Resource-based invoke permissions Only when used
lambda:PublishLayerVersion, lambda:GetLayerVersion, lambda:DeleteLayerVersion Layer versions Only when used
lambda:CreateFunctionUrlConfig, lambda:GetFunctionUrlConfig, lambda:UpdateFunctionUrlConfig, lambda:DeleteFunctionUrlConfig Function URL Only when create_function_url = true
lambda:CreateAlias, lambda:GetAlias, lambda:UpdateAlias, lambda:DeleteAlias Aliases Only when used
iam:PassRole (scoped to the exact role ARN, condition iam:PassedToService = lambda.amazonaws.com) Passing the execution role to Lambda on every create/update Mandatory. Missing this fails the first apply with AccessDenied on iam:PassRole — not on any lambda:* action. Never wildcard the resource.
kms:DescribeKey, kms:CreateGrant CMK-encrypted env vars / package Only when kms_key_arn / source_kms_key_arn is a customer-managed key. The CMK's key policy must independently allow the execution role to kms:Decrypt.

ℹ️ ec2:CreateNetworkInterface / DescribeNetworkInterfaces / DeleteNetworkInterface (VPC functions) and ecr:BatchGetImage / GetDownloadUrlForLayer (image functions) are exercised by the Lambda service via the execution role at invoke time, not by the Terraform identity — grant them on the role (terraform-aws-iam-role), not here.


📋 AWS Prerequisites

  • Service-linked roles: None created by this module. Standard function creation needs no SLR; VPC functions use AWS-managed Hyperplane ENIs (no caller action).
  • Execution role trust policy (prerequisite, not created here): the role passed as role must trust lambda.amazonaws.com to sts:AssumeRole — owned by terraform-aws-iam-role.
  • Container images (package_type = "Image", the default): the ECR repository must already hold a pushed image at image_uri, and its repository policy must grant the Lambda service pull access — otherwise create/update fails at the manifest-validation step.
  • Reserved concurrency draws from the account pool (default 1,000 concurrent executions per Region, adjustable via Service Quotas). A reservation reduces the shared unreserved pool for every other function — coordinate before setting a large value.
  • Function URLs are a public network surface even with AWS_IAM auth (IAM gates who, not network reach). There is no way to restrict a function URL to a VPC — use API Gateway + VPC Link for network isolation.
  • Layers count against the 250 MB unzipped package limit (code + up to 5 layers) and the per-Region layer-storage quota (75 GB, soft).
  • Event source mappings require the execution role (not the Terraform identity) to hold the source's read/poll permissions (sqs:ReceiveMessage/DeleteMessage/GetQueueAttributes, or the stream-read equivalents).
  • Region: no us-east-1 constraint for Lambda itself. (Lambda@Edge — a distinct CloudFront-associated model with a us-east-1 publishing constraint — is explicitly out of scope.)

📁 Module Structure

terraform-aws-lambda/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # function_name, role, package_type set, config, child maps, tags, timeouts
├── main.tf # locals + aws_lambda_function.this + 5 child resources
├── outputs.tf # id + arn first, function attrs, function URL, child maps, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, Consumes/Emits, IAM, prerequisites, gotchas

⚙️ Quick Start

module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "casey-loan-events"
  role          = module.lambda_exec_role.arn # terraform-aws-iam-role (trusts lambda.amazonaws.com)

  # Container-image deployment (the module default)
  package_type = "Image"
  image_uri    = "${module.ecr.repository_url}@sha256:abc123..." # terraform-aws-ecr

  memory_size = 512
  timeout     = 30

  environment_variables = { LOG_LEVEL = "INFO" }
  kms_key_arn           = module.kms.arn # terraform-aws-kms — CMK for env vars

  tags = { Environment = "prod", App = "lending" }
}

⚠️ The Terraform identity must have iam:PassRole on module.lambda_exec_role.arn (condition iam:PassedToService = lambda.amazonaws.com), or the first apply fails on PassRole. Pin the source with ?ref=v1.0.0 — never a branch.


🔌 Cross-Module Contract

Consumes

Input Type Source module
role string (ARN, required) terraform-aws-iam-role (execution role)
image_uri string (Image deploys) terraform-aws-ecr
s3_bucket / s3_key / s3_object_version string (Zip deploys) terraform-aws-s3-bucket
kms_key_arn / source_kms_key_arn string (ARN) terraform-aws-kms
vpc_config.subnet_ids list(string) terraform-aws-vpc
vpc_config.security_group_ids list(string) terraform-aws-security-group
dead_letter_target_arn string (ARN) terraform-aws-sqs / terraform-aws-sns
event_source_mappings[*].event_source_arn string (ARN) terraform-aws-sqs, terraform-aws-kinesis-stream, terraform-aws-dynamodb, terraform-aws-msk, terraform-aws-mq
file_system_config.arn string (ARN) terraform-aws-efs (access point)
logging_config.log_group string terraform-aws-cloudwatch-log-group
code_signing_config_arn string (ARN) code-signing config (out of scope)

Emits

Output Description Consumed by
id Function id (= function name) reference lookups
arn Unqualified function ARN terraform-aws-iam-policy (resource ARNs), EventBridge / S3 / SNS trigger targets
function_name Function name aws_lambda_permission, event source mappings, CLI/SDK
invoke_arn / qualified_invoke_arn API Gateway integration URIs API Gateway integrations
qualified_arn / version Version pinning alias targets, blue/green tooling
function_url / function_url_id HTTPS endpoint (when enabled) API consumers, DNS/CNAME
layer_version_arns Map of published layer ARNs other functions consuming shared layers
alias_arns / alias_invoke_arns Map of alias ARNs aws_lambda_permission.qualifier, integrations
event_source_mapping_uuids / event_source_mapping_arns Map of mapping ids/ARNs targeted enable/disable, imports
tags_all All tags incl. provider default_tags governance / audit

📚 Example Library

1 · Minimal — container image (module default)
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "image-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."
}
2 · Zip deployment from a local file
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "zip-fn"
  role          = module.exec_role.arn

  package_type     = "Zip"
  runtime          = "python3.13"
  handler          = "index.handler"
  filename         = "${path.module}/build/fn.zip"
  source_code_hash = filebase64sha256("${path.module}/build/fn.zip")
}
3 · Zip deployment from S3
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "s3-zip-fn"
  role          = module.exec_role.arn

  package_type = "Zip"
  runtime      = "nodejs20.x"
  handler      = "app.handler"
  s3_bucket    = module.artifacts.id # terraform-aws-s3-bucket
  s3_key       = "lambda/fn-1.4.2.zip"
}
4 · Environment variables encrypted with a customer-managed KMS key
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "kms-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  environment_variables = {
    TABLE_NAME = module.table.name
    LOG_LEVEL  = "INFO"
  }
  kms_key_arn = module.kms.arn # CMK policy must allow the EXECUTION ROLE to kms:Decrypt
}

💡 The CMK is applied only because environment_variables is non-empty — supplying a key with no variables would be silently dropped by the API and show as perpetual drift.

5 · VPC-attached function (private-subnet placement)
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "vpc-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  vpc_config = {
    subnet_ids         = values(module.vpc.private_subnet_ids)
    security_group_ids = [module.lambda_sg.id]
  }

  # Unblock the Hyperplane-ENI destroy-ordering delay
  replace_security_groups_on_destroy = true
  replacement_security_group_ids     = [module.vpc.default_security_group_id]
}
6 · SQS event source with partial-batch responses + DLQ
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "sqs-consumer"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  dead_letter_target_arn = module.dlq.arn # terraform-aws-sqs / -sns

  event_source_mappings = {
    orders = {
      event_source_arn                   = module.orders_queue.arn # terraform-aws-sqs
      batch_size                         = 10
      maximum_batching_window_in_seconds = 5
      function_response_types            = ["ReportBatchItemFailures"]
      scaling_config_maximum_concurrency = 20
    }
  }
}
7 · Kinesis stream trigger with on-failure destination + event filtering
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "kinesis-processor"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  event_source_mappings = {
    events = {
      event_source_arn               = module.stream.arn # terraform-aws-kinesis-stream
      starting_position              = "LATEST"
      parallelization_factor         = 2
      maximum_retry_attempts         = 3
      bisect_batch_on_function_error = true
      filters                        = ["{ \"data\": { \"type\": [\"payment\"] } }"]
      on_failure_destination_arn     = module.failures_queue.arn
    }
  }
}
8 · Function URL with AWS_IAM auth + CORS
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "url-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  create_function_url             = true
  function_url_authorization_type = "AWS_IAM" # default — SigV4-signed callers only
  function_url_invoke_mode        = "RESPONSE_STREAM"
  function_url_cors = {
    allow_origins = ["https://app.financialpartners.com"]
    allow_methods = ["GET", "POST"]
    max_age       = 3600
  }

  # A function URL with AWS_IAM still needs an explicit invoke grant per principal
  permissions = {
    portal = {
      principal              = "123456789012"
      action                 = "lambda:InvokeFunctionUrl"
      function_url_auth_type = "AWS_IAM"
    }
  }
}
9 · Public function URL (NONE auth) — secure-default opt-out (requires exception)
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "public-webhook"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  create_function_url             = true
  function_url_authorization_type = "NONE" # PUBLIC + UNAUTHENTICATED — document the regulated-industry exception
}

⚠️ NONE creates a public invoke grant that AWS does not remove on destroy, and must never front PII-bearing workloads. Prefer AWS_IAM or API Gateway with an authorizer.

10 · Resource-based permission — let S3 invoke the function
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "s3-notify"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  permissions = {
    from_bucket = {
      principal      = "s3.amazonaws.com"
      source_arn     = module.uploads.arn # terraform-aws-s3-bucket
      source_account = "123456789012"
    }
  }
}
11 · Published version with aliases (blue/green + canary weights)
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "aliased-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  publish = true # required so aliases can point at a real version, not $LATEST

  aliases = {
    live = {
      # function_version defaults to the latest published version
      description                = "Production traffic"
      additional_version_weights = { "2" = 0.1 } # 10% canary to version 2
    }
    staging = { function_version = "3" }
  }
}
12 · Publishing a layer and attaching it
# One module call publishes a shared layer (alongside its own function)...
module "base_fn" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "base-fn"
  role          = module.exec_role.arn

  package_type = "Zip"
  runtime      = "python3.13"
  handler      = "index.handler"
  filename     = "${path.module}/build/fn.zip"

  layer_versions = {
    deps = {
      filename            = "${path.module}/build/deps-layer.zip"
      compatible_runtimes = ["python3.13"]
      description         = "Shared Python dependencies"
    }
  }
}

#...and other function modules attach it by ARN.
module "worker_fn" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "worker-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."
  layers        = [module.base_fn.layer_version_arns["deps"]]
}

ℹ️ var.layer_versions publishes new layers; var.layers attaches existing layer ARNs. A function cannot attach a layer published by its own module call (that would be a dependency cycle) — publish in one call and attach from other function modules, as shown.

13 · X-Ray tracing, DLQ, reserved concurrency, and JSON logging
module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "observable-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  tracing_mode                   = "Active"
  dead_letter_target_arn         = module.dlq.arn
  reserved_concurrent_executions = 50

  logging_config = {
    log_format            = "JSON"
    application_log_level = "INFO"
    system_log_level      = "WARN"
    log_group             = module.fn_log_group.name # terraform-aws-cloudwatch-log-group
  }

  tags = { Team = "payments" }
}
14 · Tags — merge with provider default_tags
provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = { Owner = "platform", ManagedBy = "terraform" }
  }
}

module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "tagged-fn"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  tags = { Environment = "prod", Owner = "serverless-team" } # overrides default_tags Owner

  event_source_mappings = {
    q = {
      event_source_arn = module.queue.arn
      tags             = { Trigger = "orders" } # merged over module tags on this mapping
    }
  }
}
# module.lambda.tags_all => { Owner="serverless-team", ManagedBy="terraform", Environment="prod" }
15 · End-to-end composition — role + ECR + KMS + VPC + queue (finale)
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-lambda"
}

module "exec_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  name   = "casey-loan-events-exec"
  # assume_role_policy: trusts lambda.amazonaws.com;
  # attach AWSLambdaBasicExecutionRole + SQS read + kms:Decrypt on module.kms.arn
}

module "ecr" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-ecr?ref=v1.0.0"
  name   = "casey-loan-events"
}

module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"
  name   = "casey-orders"
}

module "dlq" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"
  name   = "casey-orders-dlq"
}

module "lambda" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-lambda?ref=v1.0.0"

  function_name = "casey-loan-events"
  role          = module.exec_role.arn
  image_uri     = "${module.ecr.repository_url}@sha256:abc123..."

  memory_size = 1024
  timeout     = 60

  environment_variables = { LOG_LEVEL = "INFO" }
  kms_key_arn           = module.kms.arn

  vpc_config = {
    subnet_ids         = values(module.vpc.private_subnet_ids) # terraform-aws-vpc
    security_group_ids = [module.lambda_sg.id]                 # terraform-aws-security-group
  }

  dead_letter_target_arn = module.dlq.arn
  tracing_mode           = "Active"

  event_source_mappings = {
    orders = {
      event_source_arn        = module.orders_queue.arn
      batch_size              = 10
      function_response_types = ["ReportBatchItemFailures"]
    }
  }

  tags = { Environment = "prod", App = "lending-portal" }
}

📥 Inputs

ℹ️ High-level grouping:

  • Identity / required: function_name, role
  • Deployment package: package_type (Image | Zip), image_uri, image_config, filename, s3_bucket, s3_key, s3_object_version, handler, runtime, architectures, layers, source_code_hash, source_kms_key_arn, code_signing_config_arn
  • Runtime config: description, memory_size, timeout, ephemeral_storage_size, reserved_concurrent_executions, publish, snap_start_apply_on
  • Encryption / env: environment_variables, kms_key_arn
  • Networking: vpc_config ({ subnet_ids, security_group_ids, ipv6_allowed_for_dual_stack? }), replace_security_groups_on_destroy, replacement_security_group_ids
  • Reliability / observability: dead_letter_target_arn, tracing_mode, logging_config, file_system_config
  • Function URL: create_function_url, function_url_authorization_type, function_url_invoke_mode, function_url_qualifier, function_url_cors
  • Child collections (for_each maps): event_source_mappings, permissions, layer_versions, aliases
  • Universal: tags, timeouts

🧾 Outputs

  • Primary: id, arn
  • Function attributes: function_name, invoke_arn, qualified_arn, qualified_invoke_arn, version, last_modified, source_code_size, signing_job_arn
  • Function URL (conditional, try(...) → null): function_url, function_url_id, function_url_invoke_mode
  • Child maps: event_source_mapping_uuids, event_source_mapping_arns, permission_statement_ids, layer_version_arns, layer_version_layer_arns, layer_version_versions, alias_arns, alias_invoke_arns
  • Tags: tags_all

ℹ️ No outputs are marked sensitive — this module emits no secrets. It never emits environment-variable values.


🧠 Architecture Notes

  • ARN / ID formats:
  • id → the function name (Lambda's id is the name, not an opaque id).
  • arnarn:aws:lambda:<region>:<account>:function:<name> (unqualified) — the cross-resource reference type.
  • qualified_arn…:function:<name>:<version>; invoke_arn is the API Gateway integration URI.
  • Layer ARNs → …:layer:<name>:<version> (versioned) and …:layer:<name> (unversioned).
  • Force-new / immutable fields: function_name and package_type force replacement of the function; a layer's layer_name forces a new layer. Each aws_lambda_layer_version is immutable — changing its contents publishes a new version.
  • tagstags_alldefault_tags: var.tags is written to the function and each event source mapping (the only taggable resources); per-mapping tags merge on top. tags_all is the provider-computed union over default_tags with resource tags winning. default_tags is the caller's provider-block concern.
  • kms_key_arn is asymmetric. Supplied with no environment variables → the API silently drops it (perpetual drift). Omitted with env variables → the AWS-managed service key is used. This module renders kms_key_arn only when environment_variables is non-empty.
  • reserved_concurrent_executions: null/unset = unreserved (-1); 0 = a valid kill switch that disables all invocations; a positive value reserves from (and shrinks) the shared account pool.
  • Function URL NONE auth leaves a dangling grant. AWS auto-adds public lambda:InvokeFunctionUrl permissions on creation that it does not remove on destroy — the module defaults auth to AWS_IAM specifically to avoid ever creating that.
  • Aliases cannot point at $LATEST. function_version defaults to the function's latest published version, so publish = true (or an explicit version) is required for a working alias.
  • Eventual consistency / destroy ordering: VPC-attached functions leave Hyperplane ENIs that can lag deletion by minutes, blocking dependent SG/subnet destroys — use replace_security_groups_on_destroy + replacement_security_group_ids. Newly created execution roles may not be assumable immediately (IAM eventual consistency); a first apply can transiently fail and succeed on retry.
  • No us-east-1 constraint — Lambda is a regional service; the module inherits the caller's region and declares no region variable.

🧱 Design Principles

Secure-by-default posture and the explicit opt-out for each:

Hardened default Behavior Opt-out / control
Deployment type package_type = "Image" — immutable ECR digest, ECR scanning/lifecycle package_type = "Zip" (fully supported alternative)
Env-var encryption CMK when kms_key_arn supplied; AWS-managed key otherwise (never plaintext) kms_key_arn = null accepts the AWS-managed key
Function URL Not created create_function_url = true
Function URL auth AWS_IAM when a URL exists function_url_authorization_type = "NONE" (public — documented exception)
DLQ Wired whenever dead_letter_target_arn is set (presence is the switch) leave dead_letter_target_arn = null
VPC placement Supported, not forced (no-ENI is often more secure for pure event processing) supply vpc_config to attach to private subnets
Reserved concurrency null (unreserved) — a too-low reservation is itself a self-DoS set explicitly, or 0 to hard-disable
Tracing Not forced to Active (per-trace cost, workload decision) tracing_mode = "Active"
Code signing Not enforced supply code_signing_config_arn

Other principles: one composite owns the function plus its directly-attached sub-resources; child collections are for_each maps keyed by stable strings (never count); the function URL is a conditional singleton (0-or-1 map) mirroring the aws_internet_gateway pattern; the execution role and DLQ are consumed by ARN so least-privilege policy and DLQ retention/encryption stay with their owning modules; Lambda@Edge is deliberately excluded to keep CloudFront's us-east-1 constraint out of every call site.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output

⚠️ plan / apply require valid AWS credentials and a region, plus iam:PassRole on the execution role. Always pin the module source with ?ref=v1.0.0, never a branch.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity (the package_type cross-field validation surfaces a bad Image/Zip argument set here, not mid-apply).
  • terraform fmt -check — formatting.
  • terraform plan against a sandbox account — confirm the function, event source mappings, and permissions plan as expected and that kms_key_arn renders only when env vars are present.
  • After apply, confirm arn / version are populated and (if enabled) function_url is reachable under its auth model.
  • Destroy test in a throwaway account for any VPC-attached function to validate ENI-cleanup timing before relying on it in shared environments.

💬 Example Output

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:lambda:us-east-1:123456789012:function:casey-loan-events"
id = "casey-loan-events"
function_name = "casey-loan-events"
invoke_arn = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:casey-loan-events/invocations"
version = "$LATEST"
event_source_mapping_uuids = { "orders" = "a1b2c3d4-...." }
alias_arns = {}
tags_all = { "Environment" = "prod", "App" = "lending-portal" }

🔍 Troubleshooting

  • AccessDenied on iam:PassRole (first apply): the Terraform identity lacks iam:PassRole on the role ARN. Add it scoped to that exact ARN with iam:PassedToService = lambda.amazonaws.com — this fails on PassRole, not on any lambda:* action.
  • Perpetual drift on kms_key_arn: a CMK was supplied to a function with no environment variables — the API drops it. This module only wires the key when environment_variables is non-empty; if you still see drift, ensure variables are actually set.
  • InvalidParameterValueException: The runtime parameter … is required: a Zip function is missing runtime/handler. The module's package_type validation catches this at plan time — re-run validate.
  • Image function fails on create: the ECR image at image_uri is missing, or the ECR repository policy does not grant the Lambda service pull access. Confirm the digest/tag exists and the repo policy allows lambda.amazonaws.com.
  • kms:Decrypt AccessDenied at invoke time (not apply time): the CMK key policy must allow the execution role to decrypt — the Terraform identity's KMS permissions do not cover runtime decryption.
  • DependencyViolation deleting a security group / subnet after destroying a VPC function: Hyperplane ENI cleanup lag. Set replace_security_groups_on_destroy = true with replacement_security_group_ids, or wait for the ENI to be reclaimed and retry.
  • Function URL still invokable after terraform destroy: a NONE-auth URL leaves a public grant AWS does not remove. Avoid NONE for anything sensitive; clean up the residual permission manually if it was ever created.
  • Alias apply fails targeting $LATEST: set publish = true (or a concrete function_version) — an alias cannot point at $LATEST.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): no valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.

🔗 Related Docs

  • Terraform Registry — hashicorp/aws: aws_lambda_function, aws_lambda_event_source_mapping, aws_lambda_permission, aws_lambda_layer_version, aws_lambda_function_url, aws_lambda_alias
  • AWS — AWS Lambda Developer Guide (execution role, iam:PassRole, environment-variable encryption)
  • AWS — Configuring a Lambda function to access resources in a VPC (Hyperplane ENIs, destroy timing)
  • AWS — Lambda function URLs (auth models, public-exposure notes)
  • AWS — Using AWS Lambda with Amazon SQS / Kinesis / DynamoDB / MSK (event source mappings, execution-role permissions)
  • terraform-aws-iam-role, terraform-aws-ecr, terraform-aws-kms, terraform-aws-vpc, terraform-aws-security-group, terraform-aws-sqs, terraform-aws-sns, terraform-aws-cloudwatch-log-group (sibling modules)

🧡 "Infrastructure as Code should be standardized, consistent, and secure."