Skip to content

About

Terraform module for AWS Lambda in self-managed S3 reference mode, with GitHub OIDC CI/CD deploy role and a human deny policy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AWS Lambda S3 Reference-Mode Terraform module

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.

Usage

From the Terraform Registry

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"
  }
}

From Git, pinned to a release tag

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.

With API Gateway and Step Functions wiring

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.

What it creates

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

Examples

  • basic - Lambda in reference mode with an S3 artifact bucket, a GitHub OIDC deploy role, and a human deny policy

Known limitations

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.

Requirements

  • Terraform 1.5 or later, with AWS provider 6.0 or later.
  • bash and AWS CLI 2.35.12 or later on the machine that runs terraform apply. The reference-mode switch runs scripts/set-reference-mode.sh through bash, 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 have bash.
  • The applier needs lambda:UpdateFunctionCode, lambda:GetFunction, lambda:GetFunctionConfiguration, and s3:PutObject on the bucket.

Requirements

Name Version
terraform >= 1.5.0
aws >= 6.0.0

Providers

Name Version
aws 6.69.0

Modules

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

Resources

Name Type
aws_caller_identity.current data source
aws_partition.current data source
aws_region.current data source

Inputs

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({
principal = string
source_arn = optional(string)
source_account = optional(string)
}))
{} 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({
routes = optional(map(object({
route_key = string
})), { default = { route_key = "ANY /{proxy+}" } })
stage_name = optional(string, "$default")
payload_format_version = optional(string, "2.0")
cors_configuration = optional(object({
allow_origins = optional(list(string), [])
allow_methods = optional(list(string), [])
allow_headers = optional(list(string), [])
expose_headers = optional(list(string), [])
allow_credentials = optional(bool, false)
max_age = optional(number)
}))
log_retention_days = optional(number, 30)
})
{} no
architectures Instruction set. The package must be built for the same one. list(string)
[
"x86_64"
]
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)
[
"environment:main"
]
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({
state_machine_type = optional(string, "STANDARD")
definition = optional(string)
additional_policy_json = optional(string)
log_level = optional(string, "ERROR")
log_retention_days = optional(number, 30)
})
{} no
tags Tags applied to all resources that support them. map(string) {} no
timeout Timeout in seconds (maximum 900). number 30 no

Outputs

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.

Authors

Maintained by vishnu.

License

MIT Licensed. See LICENSE.

About

Terraform module for AWS Lambda in self-managed S3 reference mode, with GitHub OIDC CI/CD deploy role and a human deny policy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages