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.
- ⚡ Provisions an
aws_lambda_functionkeystone 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 inheritsterraform-aws-ecrscanning/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 mandatoryiam:PassRolegrant impossible to miss (the #1 apply-time failure for Lambda). - 🔐 Environment variables encrypted at rest — with a customer-managed
kms_key_arnwhen 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 thereplace_security_groups_on_destroyescape 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_eachmap — 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_IAMauth — publicNONEauth is a documented, deliberate opt-out only. - 🏷️ Universal tagging —
var.tagsflows to the function and its event source mappings (the only taggable resources in the schema);tags_allsurfaced 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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
| 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_functionandaws_lambda_event_source_mappingexpose atagsargument in the v6 provider schema — the module deliberately does not attempt to tag permissions, layers, URLs, or aliases.
| 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.
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) andecr: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.
- 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
rolemust trustlambda.amazonaws.comtosts:AssumeRole— owned byterraform-aws-iam-role. - Container images (
package_type = "Image", the default): the ECR repository must already hold a pushed image atimage_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_IAMauth (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-1constraint for Lambda itself. (Lambda@Edge — a distinct CloudFront-associated model with aus-east-1publishing constraint — is explicitly out of scope.)
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
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 haveiam:PassRoleonmodule.lambda_exec_role.arn(conditioniam:PassedToService = lambda.amazonaws.com), or the first apply fails on PassRole. Pin the source with?ref=v1.0.0— never a branch.
| 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) |
| 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 |
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_variablesis 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
}
⚠️ NONEcreates a public invoke grant that AWS does not remove on destroy, and must never front PII-bearing workloads. PreferAWS_IAMor 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_versionspublishes new layers;var.layersattaches 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" }
}ℹ️ 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_eachmaps):event_source_mappings,permissions,layer_versions,aliases - Universal:
tags,timeouts
- 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.
- ARN / ID formats:
id→ the function name (Lambda'sidis the name, not an opaque id).arn→arn:aws:lambda:<region>:<account>:function:<name>(unqualified) — the cross-resource reference type.qualified_arn→…:function:<name>:<version>;invoke_arnis the API Gateway integration URI.- Layer ARNs →
…:layer:<name>:<version>(versioned) and…:layer:<name>(unversioned). - Force-new / immutable fields:
function_nameandpackage_typeforce replacement of the function; a layer'slayer_nameforces a new layer. Eachaws_lambda_layer_versionis immutable — changing its contents publishes a new version. tags↔tags_all↔default_tags:var.tagsis written to the function and each event source mapping (the only taggable resources); per-mappingtagsmerge on top.tags_allis the provider-computed union overdefault_tagswith resource tags winning.default_tagsis the caller's provider-block concern.kms_key_arnis 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 renderskms_key_arnonly whenenvironment_variablesis 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
NONEauth leaves a dangling grant. AWS auto-adds publiclambda:InvokeFunctionUrlpermissions on creation that it does not remove on destroy — the module defaults auth toAWS_IAMspecifically to avoid ever creating that. - Aliases cannot point at
$LATEST.function_versiondefaults to the function's latest published version, sopublish = 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-1constraint — Lambda is a regional service; the module inherits the caller's region and declares noregionvariable.
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.
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/applyrequire valid AWS credentials and a region, plusiam:PassRoleon the execution role. Always pin the module source with?ref=v1.0.0, never a branch.
terraform init -backend=false && terraform validate— schema + reference integrity (thepackage_typecross-field validation surfaces a bad Image/Zip argument set here, not mid-apply).terraform fmt -check— formatting.terraform planagainst a sandbox account — confirm the function, event source mappings, and permissions plan as expected and thatkms_key_arnrenders only when env vars are present.- After
apply, confirmarn/versionare populated and (if enabled)function_urlis 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.
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" }
AccessDeniedoniam:PassRole(first apply): the Terraform identity lacksiam:PassRoleon theroleARN. Add it scoped to that exact ARN withiam:PassedToService = lambda.amazonaws.com— this fails on PassRole, not on anylambda:*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 whenenvironment_variablesis non-empty; if you still see drift, ensure variables are actually set. InvalidParameterValueException: The runtime parameter … is required: a Zip function is missingruntime/handler. The module'spackage_typevalidation catches this at plan time — re-runvalidate.- Image function fails on create: the ECR image at
image_uriis missing, or the ECR repository policy does not grant the Lambda service pull access. Confirm the digest/tag exists and the repo policy allowslambda.amazonaws.com. kms:DecryptAccessDenied 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.DependencyViolationdeleting a security group / subnet after destroying a VPC function: Hyperplane ENI cleanup lag. Setreplace_security_groups_on_destroy = truewithreplacement_security_group_ids, or wait for the ENI to be reclaimed and retry.- Function URL still invokable after
terraform destroy: aNONE-auth URL leaves a public grant AWS does not remove. AvoidNONEfor anything sensitive; clean up the residual permission manually if it was ever created. - Alias apply fails targeting
$LATEST: setpublish = true(or a concretefunction_version) — an alias cannot point at$LATEST. - Credential-chain failures (
NoCredentialProviders/ExpiredToken): no valid credentials resolved. SetAWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.
- 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."