Skip to content

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.

Terraformawsmoduletyperesources


🧩 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
ResourceRoleCardinalityTaggable
aws_lambda_function.thisKeystone function1
aws_lambda_event_source_mapping.thisPoll-based triggersper event_source_mappings entry
aws_lambda_permission.thisResource-based invoke grantsper permissions entry
aws_lambda_layer_version.thisLayer versions published hereper layer_versions entry
aws_lambda_function_url.thisHTTPS invoke endpoint0–1 (create_function_url)
aws_lambda_alias.thisNamed version pointersper 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

RequirementVersion
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:

ActionRequired forNotes
lambda:CreateFunction, lambda:GetFunction, lambda:GetFunctionConfiguration, lambda:UpdateFunctionCode, lambda:UpdateFunctionConfiguration, lambda:DeleteFunctionFunction lifecycleCore CRUD
lambda:PublishVersionImmutable versionsOnly when publish = true
lambda:TagResource, lambda:UntagResource, lambda:ListTagsTaggingFunction + event source mappings
lambda:CreateEventSourceMapping, lambda:GetEventSourceMapping, lambda:UpdateEventSourceMapping, lambda:DeleteEventSourceMappingEvent source mappingsOnly when used
lambda:AddPermission, lambda:GetPolicy, lambda:RemovePermissionResource-based invoke permissionsOnly when used
lambda:PublishLayerVersion, lambda:GetLayerVersion, lambda:DeleteLayerVersionLayer versionsOnly when used
lambda:CreateFunctionUrlConfig, lambda:GetFunctionUrlConfig, lambda:UpdateFunctionUrlConfig, lambda:DeleteFunctionUrlConfigFunction URLOnly when create_function_url = true
lambda:CreateAlias, lambda:GetAlias, lambda:UpdateAlias, lambda:DeleteAliasAliasesOnly 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/updateMandatory. Missing this fails the first apply with AccessDenied on iam:PassRole — not on any lambda:* action. Never wildcard the resource.
kms:DescribeKey, kms:CreateGrantCMK-encrypted env vars / packageOnly 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-ecrmemory_size=512timeout=30environment_variables={ LOG_LEVEL ="INFO" }
kms_key_arn=module.kms.arn# terraform-aws-kms — CMK for env varstags={ 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

InputTypeSource module
rolestring (ARN, required)terraform-aws-iam-role (execution role)
image_uristring (Image deploys)terraform-aws-ecr
s3_bucket / s3_key / s3_object_versionstring (Zip deploys)terraform-aws-s3-bucket
kms_key_arn / source_kms_key_arnstring (ARN)terraform-aws-kms
vpc_config.subnet_idslist(string)terraform-aws-vpc
vpc_config.security_group_idslist(string)terraform-aws-security-group
dead_letter_target_arnstring (ARN)terraform-aws-sqs / terraform-aws-sns
event_source_mappings[*].event_source_arnstring (ARN)terraform-aws-sqs, terraform-aws-kinesis-stream, terraform-aws-dynamodb, terraform-aws-msk, terraform-aws-mq
file_system_config.arnstring (ARN)terraform-aws-efs (access point)
logging_config.log_groupstringterraform-aws-cloudwatch-log-group
code_signing_config_arnstring (ARN)code-signing config (out of scope)

Emits

OutputDescriptionConsumed by
idFunction id (= function name)reference lookups
arnUnqualified function ARNterraform-aws-iam-policy (resource ARNs), EventBridge / S3 / SNS trigger targets
function_nameFunction nameaws_lambda_permission, event source mappings, CLI/SDK
invoke_arn / qualified_invoke_arnAPI Gateway integration URIsAPI Gateway integrations
qualified_arn / versionVersion pinningalias targets, blue/green tooling
function_url / function_url_idHTTPS endpoint (when enabled)API consumers, DNS/CNAME
layer_version_arnsMap of published layer ARNsother functions consuming shared layers
alias_arns / alias_invoke_arnsMap of alias ARNsaws_lambda_permission.qualifier, integrations
event_source_mapping_uuids / event_source_mapping_arnsMap of mapping ids/ARNstargeted enable/disable, imports
tags_allAll tags incl. provider default_tagsgovernance / 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.arnimage_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.arnpackage_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.arnpackage_type="Zip"runtime="nodejs20.x"handler="app.handler"s3_bucket=module.artifacts.id# terraform-aws-s3-buckets3_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.arnimage_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.arnimage_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 delayreplace_security_groups_on_destroy=truereplacement_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."dead_letter_target_arn=module.dlq.arn# terraform-aws-sqs / -snsevent_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.arnimage_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."create_function_url=truefunction_url_authorization_type="AWS_IAM"# default — SigV4-signed callers onlyfunction_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 principalpermissions={
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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."create_function_url=truefunction_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.arnimage_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."publish=true# required so aliases can point at a real version, not $LATESTaliases={
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.arnpackage_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.arnimage_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."tracing_mode="Active"dead_letter_target_arn=module.dlq.arnreserved_concurrent_executions=50logging_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."tags={ Environment ="prod", Owner ="serverless-team" } # overrides default_tags Ownerevent_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.arnimage_uri="${module.ecr.repository_url}@sha256:abc123..."memory_size=1024timeout=60environment_variables={ LOG_LEVEL ="INFO" }
kms_key_arn=module.kms.arnvpc_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.arntracing_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_arnonly 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 defaultBehaviorOpt-out / control
Deployment typepackage_type = "Image" — immutable ECR digest, ECR scanning/lifecyclepackage_type = "Zip" (fully supported alternative)
Env-var encryptionCMK when kms_key_arn supplied; AWS-managed key otherwise (never plaintext)kms_key_arn = null accepts the AWS-managed key
Function URLNot createdcreate_function_url = true
Function URL authAWS_IAM when a URL existsfunction_url_authorization_type = "NONE" (public — documented exception)
DLQWired whenever dead_letter_target_arn is set (presence is the switch)leave dead_letter_target_arn = null
VPC placementSupported, not forced (no-ENI is often more secure for pure event processing)supply vpc_config to attach to private subnets
Reserved concurrencynull (unreserved) — a too-low reservation is itself a self-DoSset explicitly, or 0 to hard-disable
TracingNot forced to Active (per-trace cost, workload decision)tracing_mode = "Active"
Code signingNot enforcedsupply 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."