Terraform module which creates a Lambda function that runs in reference mode: the function reads its deployment package directly from an S3 bucket you own, and GitHub Actions deploys new code through an OIDC-trusted role. The module also provisions the artifact bucket, the IAM roles, the GitHub OIDC trust, and an optional deny policy that blocks direct console or CLI edits.
Terraform owns the infrastructure and configuration. The pipeline owns the code.
module "lambda_function" {
source = "aquavis12/lambda-s3-reference/aws"
version = "~> 1.2"
function_name = "my-function"
handler = "main.handler"
seed_zip_path = "${path.module}/seed/bootstrap.zip"
github_repository = "aquavis12/my-repo"
github_subjects = ["environment:main"]
tags = {
Terraform = "true"
Environment = "dev"
}
}module "lambda_function" {
source = "git::https://github.com/aquavis12/terraform-aws-lambda-s3-reference.git?ref=v1.2.1"
function_name = "my-function"
handler = "main.handler"
seed_zip_path = "${path.module}/seed/bootstrap.zip"
github_repository = "aquavis12/my-repo"
github_subjects = ["environment:main"]
}seed_zip_path must point at a real zip built for your runtime and
architecture. After the first apply, the pipeline owns the code and Terraform
ignores code changes. Set the module outputs artifact_bucket and
deploy_role_arn as ARTIFACT_BUCKET and AWS_DEPLOY_ROLE_ARN in the GitHub
Environment, and set FUNCTION_NAME to function_name.
Both are optional and off by default. Enabling them does not change the reference-mode contract: they add only invoke permissions and caller resources, never the function's code.
module "lambda_function" {
source = "aquavis12/lambda-s3-reference/aws"
version = "~> 1.0"
function_name = "my-function"
handler = "main.handler"
seed_zip_path = "${path.module}/seed/bootstrap.zip"
github_repository = "aquavis12/my-repo"
# Front the function with an HTTP API (API Gateway v2, AWS_PROXY).
enable_api_gateway = true
api_gateway = {
routes = {
items = { route_key = "GET /items" }
create = { route_key = "POST /items" }
}
cors_configuration = {
allow_origins = ["https://app.example.com"]
allow_methods = ["GET", "POST"]
}
}
# Create a state machine that invokes the function.
enable_step_functions = true
step_functions = {
state_machine_type = "STANDARD"
# definition = file("${path.module}/workflow.asl.json") # or use the built-in single-task default
}
}The HTTP API invoke URL is exposed as the api_endpoint output, and the state
machine as state_machine_arn. For any other caller (S3 notifications,
EventBridge, SNS), grant invoke rights with the allowed_invokers input instead
of wiring an aws_lambda_permission by hand.
| Resource | Purpose |
|---|---|
| S3 bucket | Artifact store. Versioned, encrypted with SSE-S3, public access blocked |
| Bucket policy | Lets the Lambda service read the artifact prefix, scoped to this function. Optionally denies deletes for everyone |
| Seed object | The first package, uploaded from seed_zip_path |
| Lambda function | Created from the seed package, then switched to reference mode |
| Log group | Retention set by log_retention_days |
| Execution role | Basic logging, plus inline policies you supply (execution_policy_json, or execution_policy_jsons keyed per concern) and any managed_policy_arns you attach |
| Deploy role | Assumed by GitHub Actions through OIDC. Can update code and publish, nothing else |
| Deny policy | Optional. Blocks code and configuration edits by named human roles |
| HTTP API | Optional (enable_api_gateway). API Gateway v2 with an AWS_PROXY integration, access logging, and an invoke permission scoped to this API |
| State machine | Optional (enable_step_functions). Step Functions workflow with a dedicated role allowed to invoke only this function |
- basic - Lambda in reference mode with an S3 artifact bucket, a GitHub OIDC deploy role, and a human deny policy
Reference mode is a local-exec step. The AWS provider does not yet expose
s3_object_storage_mode on aws_lambda_function. The terraform_data.reference_mode
resource runs scripts/set-reference-mode.sh through bash instead, and
re-runs when the seed package changes. When the provider adds the argument,
replace that resource with the argument and remove the script.
Code is ignored after creation. ignore_changes covers the code attributes,
so Terraform never changes code on later applies. Deploy code through the
pipeline.
Destroy needs a manual step when deletion is denied. With
deny_artifact_deletion = true, no principal can delete objects or versions, so
terraform destroy cannot empty the bucket. To tear down: apply with
deny_artifact_deletion = false (or detach the deny), delete all object
versions and delete markers, then run terraform destroy. This is intentional
friction; do it under a change request.
Noncurrent-version expiry is off by default. noncurrent_version_expiry_days
is null. A lifecycle rule that expires old versions can break a published
version that still points at them. Turn it on only after checking the versions
in use.
The deny policy also blocks configuration edits. Once a human-deny policy is
attached, UpdateFunctionConfiguration is blocked for those roles too. Changes
to memory, timeout, or layers need the policy detached first.
API Gateway cannot redeploy on code changes from Terraform. The HTTP API
stage uses auto_deploy, so route and integration changes ship automatically.
But code changes are invisible to Terraform by design (reference mode), so a new
function build does not trigger an API redeploy from here. If you need that,
drive it from the pipeline after the upload.
- Terraform 1.5 or later, with AWS provider 6.0 or later.
bashand AWS CLI 2.35.12 or later on the machine that runsterraform apply. The reference-mode switch runsscripts/set-reference-mode.shthroughbash, because the AWS provider does not yet expose the storage mode. On Windows, use Git Bash or WSL. CI runners on Linux or macOS already havebash.- The applier needs
lambda:UpdateFunctionCode,lambda:GetFunction,lambda:GetFunctionConfiguration, ands3:PutObjecton the bucket.
| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
| aws | >= 6.0.0 |
| Name | Version |
|---|---|
| aws | 6.69.0 |
| Name | Source | Version |
|---|---|---|
| api_gateway | ./modules/api-gateway | n/a |
| artifact_bucket | ./modules/artifact-bucket | n/a |
| function | ./modules/function | n/a |
| github_oidc_deploy | ./modules/github-oidc-deploy | n/a |
| human_deny | ./modules/human-deny | n/a |
| step_functions | ./modules/step-functions | n/a |
| Name | Type |
|---|---|
| aws_caller_identity.current | data source |
| aws_partition.current | data source |
| aws_region.current | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| allowed_invokers | Extra resource-based permissions that let other services invoke the function, keyed by statement id. The built-in API Gateway and Step Functions wiring (enable_api_gateway, enable_step_functions) add their own permissions automatically; use this for anything you wire by hand. |
map(object({ |
{} |
no |
| api_gateway | Settings for the HTTP API, used only when enable_api_gateway is true. routes default to a catch-all so the function owns routing. cors_configuration is null (disabled) by default. See modules/api-gateway for the full shape. |
object({ |
{} |
no |
| architectures | Instruction set. The package must be built for the same one. | list(string) |
[ |
no |
| artifact_bucket_name | Name of the artifact bucket. Defaults to <function_name>-artifacts--. | string |
null |
no |
| artifact_prefix | Key prefix in the bucket that holds deployment packages. The Lambda read grant covers this prefix only. | string |
"functions/" |
no |
| create_oidc_provider | Create the GitHub OIDC provider. Leave false if the account already has one, because only one is allowed per URL. | bool |
false |
no |
| deny_artifact_deletion | Deny object deletion and versioning changes for every principal, including administrators. Recommended: a live function re-reads its object, so a deleted object makes the function Inactive. Note that with this on, terraform destroy cannot empty the bucket; see the README. |
bool |
true |
no |
| enable_api_gateway | Front the function with an HTTP API (API Gateway v2) using an AWS_PROXY integration. | bool |
false |
no |
| enable_step_functions | Create a Step Functions state machine that invokes the function. | bool |
false |
no |
| environment_variables | Environment variables for the function. | map(string) |
{} |
no |
| ephemeral_storage_mb | Size of /tmp in MB (512 to 10240). | number |
512 |
no |
| execution_policy_json | Optional inline policy JSON for the execution role (for example DynamoDB, S3 and Textract access). | string |
null |
no |
| execution_policy_jsons | Additional inline policies for the execution role, keyed by name. Prefer this over one big execution_policy_json when the function has several event sources: give each (for example "sqs", "dynamodb-streams") its own entry. Each value is an IAM policy JSON document. |
map(string) |
{} |
no |
| function_name | Name of the Lambda function. Also used to name the bucket, roles and log group. | string |
n/a | yes |
| github_repository | GitHub repository allowed to deploy, in owner/name form. | string |
n/a | yes |
| github_subjects | Subjects allowed to assume the deploy role, relative to the repository. Use "environment:" when the workflow job declares a GitHub Environment, or "ref:refs/heads/" otherwise. Keep this narrow. |
list(string) |
[ |
no |
| handler | Handler in module.function form, for example main.handler. | string |
"main.handler" |
no |
| human_role_names | IAM role names that must not edit this function directly. The deny policy is attached to each. | list(string) |
[] |
no |
| layers | ARNs of existing layer versions to attach. This module does not create layers. Layers are optional with self-managed S3 storage; you can bundle dependencies in the function zip instead. Keep large or shared compiled binaries in layers when it suits your build. |
list(string) |
[] |
no |
| log_retention_days | CloudWatch log retention in days. | number |
30 |
no |
| managed_policy_arns | ARNs of existing managed policies to attach to the execution role. | list(string) |
[] |
no |
| memory_size | Memory in MB. | number |
512 |
no |
| noncurrent_version_expiry_days | Expire old object versions after this many days. Null disables expiry. Leave null unless you have checked that no published version still references the versions you would expire. |
number |
null |
no |
| runtime | Lambda runtime. Must match the Python version the package was built for. | string |
"python3.12" |
no |
| seed_zip_path | Local path to the first deployment package. Terraform uploads it to the artifact bucket and creates the function from it. After that, the CI/CD pipeline owns the code and Terraform ignores code changes. |
string |
n/a | yes |
| step_functions | Settings for the state machine, used only when enable_step_functions is true. definition null uses the built-in single-task invoke workflow. See modules/step-functions for the full shape. |
object({ |
{} |
no |
| tags | Tags applied to all resources that support them. | map(string) |
{} |
no |
| timeout | Timeout in seconds (maximum 900). | number |
30 |
no |
| Name | Description |
|---|---|
| api_endpoint | Base invoke URL of the HTTP API, or null when API Gateway wiring is disabled. |
| api_id | ID of the HTTP API, or null when API Gateway wiring is disabled. |
| artifact_bucket | Artifact bucket name. Set this as ARTIFACT_BUCKET in the pipeline. |
| deploy_role_arn | Role for GitHub Actions. Set this as AWS_DEPLOY_ROLE_ARN in the GitHub Environment. |
| execution_role_arn | Execution role of the function. |
| function_arn | ARN of the Lambda function. |
| function_name | Name of the Lambda function. |
| reference_mode_applied | Identifier of the reference-mode switch. Changes when the seed package changes. |
| state_machine_arn | ARN of the state machine, or null when Step Functions wiring is disabled. |
Maintained by vishnu.
MIT Licensed. See LICENSE.