Architecture¶
This page describes what the module creates and why. Solid lines in the diagram are always created, dashed lines are optional or conditional on variable values.
Resource inventory¶
| Resource | File | Count | Purpose |
|---|---|---|---|
aws_lambda_function.this | lambda.tf | 1 | The function itself. Source code lives in S3. |
aws_lambda_function_event_invoke_config.this | lambda.tf | 1 | Disables async retries by default. |
module.lambda_bucket | lambda_s3.tf | 1 | S3 bucket for deployment packages (via infrahouse/s3-bucket). |
aws_s3_object.lambda_package | lambda_s3.tf | 1 | Uploaded zip. Key includes a content hash so uploads are immutable. |
aws_iam_role.lambda | lambda_iam.tf | 1 | Execution role, name_prefix-ed so long function names still fit in 38 chars. |
aws_iam_role_policy.lambda_logging | lambda_iam.tf | 1 | Write logs to the function's log group only. |
aws_iam_role_policy.lambda_vpc_access | lambda_iam.tf | 0 or 1 | ENI lifecycle in specified subnets only, when lambda_subnet_ids is set. |
aws_iam_role_policy_attachment.additional | lambda_iam.tf | N | One per ARN in additional_iam_policy_arns. |
aws_iam_role_policy_attachment.lambda_insights | lambda_iam.tf | 0 or 1 | Attaches CloudWatchLambdaInsightsExecutionRolePolicy when memory alarm is enabled. |
aws_cloudwatch_log_group.lambda | cloudwatch.tf | 1 | /aws/lambda/${function_name} with configurable retention. |
aws_sns_topic.alarms | sns.tf | 1 | Fan-out point for all alarms. |
aws_sns_topic_subscription.alarm_emails | sns.tf | N | One per email in alarm_emails. |
aws_cloudwatch_metric_alarm.errors_immediate | alarms.tf | 0 or 1 | Fires on any error when alert_strategy = "immediate". |
aws_cloudwatch_metric_alarm.errors_threshold | alarms.tf | 0 or 1 | Fires when errors/invocations * 100 > threshold (threshold strategy). |
aws_cloudwatch_metric_alarm.throttles | alarms.tf | 0 or 1 | Fires on any throttle. |
aws_cloudwatch_metric_alarm.duration | alarms.tf | 0 or 1 | Created only when duration_threshold_percent is set. |
aws_cloudwatch_metric_alarm.memory | alarms.tf | 0 or 1 | Created only when memory_utilization_threshold_percent is set (requires Lambda Insights). |
Packaging pipeline¶
Lambda source goes through a dedicated build pipeline before being uploaded to S3. Each function gets its own build directory inside the caller's root module (${path.root}/.build/${function_name}/), never inside this module.
Hashing¶
package_hash (in locals.tf) is the MD5 of:
source_files_hash— MD5 of all files matchingsource_code_files(default:["main.py"])filemd5(requirements_file)— hash of the pinned depsvar.architecture—x86_64orarm64var.python_version—python3.11,python3.12, …var.function_namemodule_version— bumped manually inlocals.tfduring releases
Any change to these triggers a repackage. Critically, the hash does not include files in .build/, so wiping .terraform and rebuilding doesn't cause a spurious re-upload.
Build¶
scripts/package.sh runs via null_resource.lambda_package with these flags:
pip install \
--only-binary=:all: \
--platform manylinux_2_28_${ARCH} \
--platform manylinux_2_17_${ARCH} \
--platform manylinux2014_${ARCH} \
--target ./.build/${function_name}/ \
-r requirements.txt
The --only-binary=:all: flag forces pip to reject source distributions so you can't accidentally ship a package that only builds on your Mac. The target platform matches the Lambda runtime, not the host.
The platform tags form a ladder, newest glibc floor first. An explicit --platform makes pip match only that tag's compatibility set — it does not fall back to lower manylinux floors — so all the acceptable floors must be listed. pip then picks the first matching wheel per package: packages that ship manylinux_2_28 (e.g. pyarrow>=21) get it, while packages that ship only manylinux_2_17 / manylinux2014 (e.g. cffi) still resolve. Since manylinux is a glibc floor and glibc is backward-compatible, every wheel here runs on the Amazon Linux 2023 runtime (glibc 2.34).
Upload¶
The zip is uploaded to ${bucket}/${function_name}/${package_hash}.zip. The local-exec provisioner on aws_s3_object.lambda_package runs wait_for_s3_object.sh to poll s3api head-object until the object is visible — this avoids race conditions where Lambda creation races ahead of S3 eventual consistency.
Alert flow¶
local.all_alarm_topic_arns (in sns.tf) combines the module-created topic with user-supplied alarm_topic_arns, and every alarm action writes to that combined list. So if you pass in a PagerDuty topic ARN, all four alarms will fan out to both the email subs and PagerDuty.
IAM role naming¶
The execution role uses name_prefix = "${substr(var.function_name, 0, 37)}-". This is because:
- AWS limits IAM role names to 64 characters
name_prefixappends a 26-character unique suffix- So the prefix must fit in
64 - 26 = 38characters, including the trailing-
Function names longer than 37 characters still work — the role is created under a truncated prefix — but the full function name is preserved in tags. Downstream callers should always use the lambda_role_arn / lambda_role_name outputs rather than trying to reconstruct the role name from function_name.