From 0338a51ae9d0e746bc3a5645008c41101617891c Mon Sep 17 00:00:00 2001 From: Tobias Babin Date: Mon, 17 Nov 2025 16:00:15 +0100 Subject: [PATCH 1/2] Moved over existing content --- README.md | 802 +++++++++++++++++++++++++++++++++++++++-- iam.tf | 57 +++ lambda.tf | 50 +++ main.tf | 8 +- outputs.tf | 35 +- tests/basic.tftest.hcl | 525 ++++++++++++++++++++++++++- variables.tf | 110 +++++- versions.tf | 15 +- 8 files changed, 1569 insertions(+), 33 deletions(-) create mode 100644 iam.tf create mode 100644 lambda.tf diff --git a/README.md b/README.md index ffccd59..4d87645 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,797 @@ -# TF module template repository +# AWS Lambda Module -Use this [GitHub template repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository) as a starting point for any new Terraform/OpenTofu (TF) module repository in this GitHub organization. +A reusable Terraform module for deploying AWS Lambda functions with flexible IAM configuration and optional Function URLs. -It provides any new repository created from it with +## Overview -- GitHub workflows for TF validation and testing -- A default file structure for the TF code and TF tests -- Automated [Terraform docs](https://github.com/terraform-docs/terraform-docs) generation +This module provides a streamlined way to deploy AWS Lambda functions with the following features: -To support Terraform docs generation in your module repository, use the markers <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS --> in your `README.md` file to designate the location of the generated TF docs inside the README. If not included, TF docs will be appended to the end of the README. Set the GitHub workflow variable `disable-tf-docs` to `false` to disable Terraform docs generation. +- Lambda function deployment from S3-stored packages +- Automatic or custom IAM role management +- Support for additional managed and inline IAM policies +- Optional Lambda Function URLs with configurable authentication +- Configurable CORS for Function URLs +- Support for multiple runtimes (Python, Node.js, Go, Java, etc.) +- Customizable timeout, memory, and architecture settings +- Environment variable configuration +- Tagging support -Find the autogenerated TF docs below. +## Prerequisites - -## Requirements +Before using this module, ensure you have: -No requirements. +1. **An S3 bucket** containing your Lambda deployment package (ZIP file) +2. **AWS credentials** configured with appropriate permissions to create Lambda functions and IAM roles +3. **Lambda deployment package** uploaded to S3 in ZIP format -## Providers +## Usage -No providers. +### Basic Usage -## Modules +Minimal configuration with automatic IAM role creation: -No modules. +```hcl +module "simple_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" -## Resources + s3_bucket = "my-deployment-bucket" + s3_key = "my-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" +} +``` -No resources. +### Lambda with Custom Configuration -## Inputs +```hcl +module "custom_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" -No inputs. + s3_bucket = "my-deployment-bucket" + s3_key = "my-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" -## Outputs + timeout_in_seconds = 300 + memory_size = 512 + architectures = ["arm64"] + + environment_variables = { + ENVIRONMENT = "production" + LOG_LEVEL = "info" + API_KEY = "your-api-key" + } + + additional_tags = { + project = "my-project" + environment = "production" + managed_by = "terraform" + } +} +``` + +### Lambda with Function URL (Public Access) + +This example creates a Lambda with a publicly accessible HTTPS endpoint: + +```hcl +module "public_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "api-handler.zip" + runtime = "nodejs20.x" + handler = "index.handler" + + # Enable Function URL with public access + enable_function_url = true + function_url_auth_type = "NONE" + + # Configure CORS for web applications + function_url_cors = { + allow_origins = ["https://example.com", "https://app.example.com"] + allow_methods = ["GET", "POST", "PUT", "DELETE"] + allow_headers = ["content-type", "x-custom-header"] + expose_headers = ["x-request-id"] + allow_credentials = true + max_age = 86400 + } +} + +output "api_url" { + value = module.public_lambda.function_url +} +``` + +### Lambda with Function URL (IAM Authentication) + +This example creates a Lambda with Function URL requiring AWS IAM authentication: + +```hcl +module "secure_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "secure-api.zip" + runtime = "python3.12" + handler = "app.handler" + + # Enable Function URL with IAM authentication + enable_function_url = true + function_url_auth_type = "AWS_IAM" +} +``` + +### Lambda with Inline IAM Policies + +Use inline policies for Lambda-specific permissions: + +```hcl +module "lambda_with_s3" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "s3-processor.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + additional_inline_policies = { + s3_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject" + ] + Resource = "arn:aws:s3:::my-data-bucket/*" + } + ] + }) + + dynamodb_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:Query" + ] + Resource = "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable" + } + ] + }) + } +} +``` + +### Lambda with Managed IAM Policies + +Attach AWS-managed or customer-managed policies: + +```hcl +module "lambda_with_managed_policies" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "processor.zip" + runtime = "nodejs20.x" + handler = "index.handler" + + additional_managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", + "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" + ] +} +``` + +### Lambda with Existing IAM Role + +If you already have an IAM role configured: + +```hcl +module "lambda_with_existing_role" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "my-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + # Use existing IAM role + iam_role_arn = "arn:aws:iam::123456789012:role/my-existing-lambda-role" + + # Note: additional_managed_policy_arns and additional_inline_policies + # will be IGNORED when iam_role_arn is provided +} +``` + +### Go Lambda with Custom Runtime + +Example for Go Lambda functions using custom runtime: + +```hcl +module "go_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + s3_bucket = "my-deployment-bucket" + s3_key = "bootstrap.zip" + + # Go uses custom runtime + runtime = "provided.al2023" + handler = "bootstrap" + + architectures = ["x86_64"] + + timeout_in_seconds = 100 + memory_size = 512 + + environment_variables = { + ENVIRONMENT = "production" + } +} +``` + +### Complete Example with All Features + +```hcl +module "full_featured_lambda" { + source = "github.com/humanitec-tf-modules/serverless-lambda?ref=vX.Y.Z" + + # Lambda package configuration + s3_bucket = "my-deployment-bucket" + s3_key = "comprehensive-function.zip" + runtime = "python3.12" + handler = "app.lambda_handler" + + # Function configuration + timeout_in_seconds = 300 + memory_size = 1024 + architectures = ["arm64"] + + # Environment variables + environment_variables = { + ENVIRONMENT = "production" + LOG_LEVEL = "info" + DATABASE_URL = "postgresql://..." + CACHE_ENABLED = "true" + } + + # Function URL configuration + enable_function_url = true + function_url_auth_type = "NONE" + + function_url_cors = { + allow_origins = ["https://app.example.com"] + allow_methods = ["GET", "POST"] + allow_headers = ["content-type"] + allow_credentials = false + max_age = 3600 + } + + # IAM policy configuration + additional_managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess" + ] + + additional_inline_policies = { + s3_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject" + ] + Resource = "arn:aws:s3:::my-bucket/*" + } + ] + }) + + sqs_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "sqs:SendMessage", + "sqs:ReceiveMessage", + "sqs:DeleteMessage" + ] + Resource = "arn:aws:sqs:us-west-2:123456789012:my-queue" + } + ] + }) + } + + # Tags + additional_tags = { + project = "my-project" + environment = "production" + team = "platform" + cost_center = "engineering" + } + + # Naming + name_prefix = "my-company-" +} + +# Outputs +output "function_name" { + value = module.full_featured_lambda.function_name +} + +output "function_arn" { + value = module.full_featured_lambda.function_arn +} + +output "function_url" { + value = module.full_featured_lambda.function_url +} + +output "role_arn" { + value = module.full_featured_lambda.role_arn +} +``` + +## IAM Permissions + +### Default Permissions + +When the module creates an IAM role (when `iam_role_arn` is `null`), it automatically attaches: + +1. **AWSLambdaBasicExecutionRole** - Allows Lambda to write logs to CloudWatch Logs +2. **S3 Access Policy** - Allows Lambda to read the deployment package from the specified S3 bucket + +### Additional Permissions + +You can add additional permissions using: + +- **`additional_managed_policy_arns`**: For AWS-managed or customer-managed policies +- **`additional_inline_policies`**: For function-specific permissions + +**Note**: Additional policies are ONLY applied when the module creates the IAM role. If you provide your own `iam_role_arn`, you must manage all permissions yourself. + +## Lambda Function URLs + +Lambda Function URLs provide a dedicated HTTP(S) endpoint for your Lambda function without requiring API Gateway. This is useful for: + +- Simple HTTP APIs +- Webhooks +- Public endpoints +- Microservices + +### Authentication Options + +- **`NONE`**: Public access, no authentication required +- **`AWS_IAM`**: Requires AWS SigV4 authentication + +### CORS Configuration + +When using Function URLs with web applications, configure CORS: + +```hcl +function_url_cors = { + allow_credentials = true # Allow cookies/auth headers + allow_origins = ["https://app.example.com"] # Allowed origins + allow_methods = ["GET", "POST", "PUT"] # Allowed HTTP methods + allow_headers = ["content-type", "authorization"] # Allowed headers + expose_headers = ["x-request-id"] # Headers exposed to browser + max_age = 86400 # Cache preflight for 24h +} +``` + +## Invoking Lambda Functions + +After deploying your Lambda function, you can invoke it in several ways: + +### 1. AWS CLI - Synchronous Invocation + +Invoke the function and wait for the response: + +```bash +# Get the function name from Terraform output +FUNCTION_NAME=$(terraform output -raw function_name) + +# Invoke with a simple payload +aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload '{"key": "value"}' \ + --region us-east-1 \ + response.json + +# View the response +cat response.json +``` + +### 2. AWS CLI - Asynchronous Invocation + +Invoke the function without waiting for the response: + +```bash +aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --invocation-type Event \ + --payload '{"key": "value"}' \ + --region us-east-1 \ + response.json +``` + +### 3. Function URL - HTTP Request (Public) + +For Lambda functions with Function URLs and `auth_type = "NONE"`: + +```bash +# Get the Function URL from Terraform output +FUNCTION_URL=$(terraform output -raw function_url) + +# Simple GET request +curl "$FUNCTION_URL" + +# POST request with JSON payload +curl -X POST "$FUNCTION_URL" \ + -H "Content-Type: application/json" \ + -d '{"key": "value", "message": "Hello Lambda"}' + +# POST with additional headers +curl -X POST "$FUNCTION_URL" \ + -H "Content-Type: application/json" \ + -H "X-Custom-Header: custom-value" \ + -d '{"action": "process", "data": {"id": 123}}' +``` + +### 4. Function URL - HTTP Request (IAM Authenticated) + +For Function URLs with `auth_type = "AWS_IAM"`, you need to sign the request: + +```bash +# Using awscurl (install with: pip install awscurl or brew install awscurl) +awscurl --service lambda \ + -X POST "$FUNCTION_URL" \ + -H "Content-Type: application/json" \ + -d '{"key": "value"}' + +# Or using aws-sigv4-proxy +# https://github.com/awslabs/aws-sigv4-proxy +``` + +### 5. Python (boto3) Invocation + +```python +import boto3 +import json + +# Create Lambda client +lambda_client = boto3.client('lambda', region_name='us-east-1') + +# Prepare payload +payload = { + "key": "value", + "message": "Hello from Python" +} + +# Synchronous invocation +response = lambda_client.invoke( + FunctionName='your-function-name', + InvocationType='RequestResponse', + Payload=json.dumps(payload) +) + +# Read response +result = json.loads(response['Payload'].read()) +print(f"Status Code: {response['StatusCode']}") +print(f"Response: {result}") + +# Asynchronous invocation +async_response = lambda_client.invoke( + FunctionName='your-function-name', + InvocationType='Event', + Payload=json.dumps(payload) +) +print(f"Async Status Code: {async_response['StatusCode']}") +``` + +### 6. Python - HTTP Request to Function URL + +```python +import requests +import json + +# For public Function URLs (auth_type = "NONE") +function_url = "https://abcdefg.lambda-url.us-east-1.on.aws/" + +payload = { + "action": "process", + "data": {"id": 123, "name": "example"} +} + +response = requests.post( + function_url, + json=payload, + headers={"Content-Type": "application/json"} +) + +print(f"Status Code: {response.status_code}") +print(f"Response: {response.json()}") + +# For IAM-authenticated Function URLs, use requests-aws4auth +from requests_aws4auth import AWS4Auth +import boto3 + +credentials = boto3.Session().get_credentials() +auth = AWS4Auth( + credentials.access_key, + credentials.secret_key, + 'us-east-1', + 'lambda', + session_token=credentials.token +) + +response = requests.post( + function_url, + json=payload, + auth=auth, + headers={"Content-Type": "application/json"} +) +``` + +### 7. Node.js (AWS SDK v3) Invocation + +```javascript +import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; + +const client = new LambdaClient({ region: "us-east-1" }); + +const payload = { + key: "value", + message: "Hello from Node.js" +}; + +// Synchronous invocation +const command = new InvokeCommand({ + FunctionName: "your-function-name", + InvocationType: "RequestResponse", + Payload: JSON.stringify(payload) +}); + +try { + const response = await client.send(command); + const result = JSON.parse(new TextDecoder().decode(response.Payload)); + + console.log("Status Code:", response.StatusCode); + console.log("Response:", result); +} catch (error) { + console.error("Error:", error); +} + +// Asynchronous invocation +const asyncCommand = new InvokeCommand({ + FunctionName: "your-function-name", + InvocationType: "Event", + Payload: JSON.stringify(payload) +}); + +const asyncResponse = await client.send(asyncCommand); +console.log("Async Status Code:", asyncResponse.StatusCode); +``` + +### 8. Node.js - HTTP Request to Function URL + +```javascript +// For public Function URLs (auth_type = "NONE") +const functionUrl = "https://abcdefg.lambda-url.us-east-1.on.aws/"; + +const payload = { + action: "process", + data: { id: 123, name: "example" } +}; + +try { + const response = await fetch(functionUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + console.log("Status:", response.status); + console.log("Response:", result); +} catch (error) { + console.error("Error:", error); +} +``` + +### 9. Testing with Sample Events + +You can test your Lambda with AWS-provided sample events: + +```bash +# Create a sample S3 event +cat > s3-event.json < \ No newline at end of file diff --git a/iam.tf b/iam.tf new file mode 100644 index 0000000..4fa5f93 --- /dev/null +++ b/iam.tf @@ -0,0 +1,57 @@ +resource "aws_iam_role" "role" { + count = local.create_lambda_role ? 1 : 0 + name_prefix = var.iam_role_name_prefix + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) + tags = var.additional_tags +} + +resource "aws_iam_role_policy_attachment" "lambda_basic" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = local.create_lambda_role ? aws_iam_role.role[0].name : split("/", var.iam_role_arn)[1] +} + +resource "aws_iam_role_policy" "s3_zip_bucket_access" { + count = local.create_lambda_role ? 1 : 0 + role = aws_iam_role.role[0].id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:GetObjectVersion" + ] + Resource = "arn:aws:s3:::${var.s3_bucket}/${var.s3_key}" + } + ] + }) +} + +# Attach additional managed policies to the Lambda role +resource "aws_iam_role_policy_attachment" "lambda_additional_managed_policies" { + for_each = local.create_lambda_role ? toset(var.additional_managed_policy_arns) : [] + + role = aws_iam_role.role[0].name + policy_arn = each.value +} + +# Attach additional inline policies to the Lambda role +resource "aws_iam_role_policy" "lambda_additional_inline_policies" { + for_each = local.create_lambda_role ? var.additional_inline_policies : {} + + role = aws_iam_role.role[0].id + name = each.key + policy = each.value +} diff --git a/lambda.tf b/lambda.tf new file mode 100644 index 0000000..c270e14 --- /dev/null +++ b/lambda.tf @@ -0,0 +1,50 @@ +resource "random_id" "entropy" { + byte_length = 4 + prefix = var.name_prefix +} + +resource "aws_lambda_function" "function" { + function_name = random_id.entropy.hex + role = local.create_lambda_role ? aws_iam_role.role[0].arn : var.iam_role_arn + + package_type = "Zip" + s3_bucket = var.s3_bucket + s3_key = var.s3_key + + handler = var.handler + runtime = var.runtime + + dynamic "environment" { + for_each = length(var.environment_variables) > 0 ? [1] : [] + content { + variables = var.environment_variables + } + } + + architectures = var.architectures + + timeout = var.timeout_in_seconds + memory_size = var.memory_size + + tags = var.additional_tags +} + +# Lambda Function URL - Creates an HTTPS endpoint for the Lambda +resource "aws_lambda_function_url" "function_url" { + count = var.enable_function_url ? 1 : 0 + + function_name = aws_lambda_function.function.function_name + authorization_type = var.function_url_auth_type + + dynamic "cors" { + for_each = var.function_url_cors != null ? [var.function_url_cors] : [] + content { + allow_credentials = cors.value.allow_credentials + allow_origins = cors.value.allow_origins + allow_methods = cors.value.allow_methods + allow_headers = cors.value.allow_headers + expose_headers = cors.value.expose_headers + max_age = cors.value.max_age + } + } +} diff --git a/main.tf b/main.tf index 4122323..2e7527e 100644 --- a/main.tf +++ b/main.tf @@ -1 +1,7 @@ -# Main TF code goes here \ No newline at end of file +data "aws_region" "current" {} + +locals { + create_lambda_role = var.iam_role_arn == null ? true : false + aws_region = data.aws_region.current.region +} + diff --git a/outputs.tf b/outputs.tf index b51961c..3db3935 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1 +1,34 @@ -# TF outputs go here \ No newline at end of file +output "function_name" { + description = "The name of the Lambda function" + value = aws_lambda_function.function.function_name +} + +output "function_arn" { + description = "The ARN of the Lambda function" + value = aws_lambda_function.function.arn +} + +output "function_url" { + description = "The HTTPS URL endpoint for the Lambda function (if enable_function_url is true)" + value = var.enable_function_url ? aws_lambda_function_url.function_url[0].function_url : null +} + +output "invoke_arn" { + description = "The ARN to be used for invoking the Lambda function from API Gateway" + value = aws_lambda_function.function.invoke_arn +} + +output "role_arn" { + description = "The ARN of the IAM role used by the Lambda function" + value = var.iam_role_arn != null ? var.iam_role_arn : aws_iam_role.role[0].arn +} + + +output "humanitec_metadata" { + description = "The Humanitec metadata annotations for the Lambda function" + value = { + Function-Arn = aws_lambda_function.function.arn + Function-Url = var.enable_function_url ? aws_lambda_function_url.function_url[0].function_url : null + Aws-Console-Url = "https://${local.aws_region}.console.aws.amazon.com/lambda/home?region=${local.aws_region}#/functions/${aws_lambda_function.function.function_name}" + } +} diff --git a/tests/basic.tftest.hcl b/tests/basic.tftest.hcl index 7fb0d7a..f941b5e 100644 --- a/tests/basic.tftest.hcl +++ b/tests/basic.tftest.hcl @@ -1,6 +1,525 @@ -# See https://developer.hashicorp.com/terraform/language/tests for more on how to write tests. -# See https://developer.hashicorp.com/terraform/language/tests/mocking for information on mocking providers. +provider "aws" { + region = "us-east-1" + skip_credentials_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + access_key = "mock_access_key" + secret_key = "mock_secret_key" +} -run "my_example_test" { +run "test_basic_lambda" { command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + } + + assert { + condition = aws_lambda_function.function.runtime == "python3.12" + error_message = "Runtime should be python3.12" + } + + assert { + condition = aws_lambda_function.function.handler == "lambda_function.lambda_handler" + error_message = "Handler should match the provided value" + } + + assert { + condition = aws_lambda_function.function.memory_size == 128 + error_message = "Default memory size should be 128" + } + + assert { + condition = aws_lambda_function.function.timeout == 300 + error_message = "Default timeout should be 300" + } + + assert { + condition = length(aws_iam_role.role) == 1 + error_message = "Should create IAM role when iam_role_arn is not provided" + } +} + +run "test_lambda_with_existing_role" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + iam_role_arn = "arn:aws:iam::123456789012:role/existing-lambda-role" + } + + assert { + condition = length(aws_iam_role.role) == 0 + error_message = "Should not create IAM role when iam_role_arn is provided" + } + + assert { + condition = aws_lambda_function.function.role == "arn:aws:iam::123456789012:role/existing-lambda-role" + error_message = "Should use the provided IAM role ARN" + } +} + +run "test_lambda_with_custom_config" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "nodejs20.x" + handler = "index.handler" + timeout_in_seconds = 60 + memory_size = 512 + architectures = ["arm64"] + } + + assert { + condition = aws_lambda_function.function.timeout == 60 + error_message = "Timeout should be 60 seconds" + } + + assert { + condition = aws_lambda_function.function.memory_size == 512 + error_message = "Memory size should be 512 MB" + } + + assert { + condition = length(aws_lambda_function.function.architectures) == 1 && aws_lambda_function.function.architectures[0] == "arm64" + error_message = "Architecture should be arm64" + } +} + +run "test_lambda_with_environment_variables" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + environment_variables = { + ENVIRONMENT = "test" + LOG_LEVEL = "debug" + API_KEY = "test-key" + } + } + + assert { + condition = aws_lambda_function.function.environment[0].variables["ENVIRONMENT"] == "test" + error_message = "Environment variable ENVIRONMENT should be 'test'" + } + + assert { + condition = aws_lambda_function.function.environment[0].variables["LOG_LEVEL"] == "debug" + error_message = "Environment variable LOG_LEVEL should be 'debug'" + } + + assert { + condition = aws_lambda_function.function.environment[0].variables["API_KEY"] == "test-key" + error_message = "Environment variable API_KEY should be 'test-key'" + } +} + +run "test_lambda_with_tags" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + additional_tags = { + project = "test-project" + environment = "development" + team = "platform" + } + } + + assert { + condition = aws_lambda_function.function.tags["project"] == "test-project" + error_message = "Tag 'project' should be 'test-project'" + } + + assert { + condition = aws_lambda_function.function.tags["environment"] == "development" + error_message = "Tag 'environment' should be 'development'" + } + + assert { + condition = aws_lambda_function.function.tags["team"] == "platform" + error_message = "Tag 'team' should be 'platform'" + } +} + +run "test_lambda_with_inline_policies" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + additional_inline_policies = { + s3_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject" + ] + Resource = "arn:aws:s3:::my-bucket/*" + } + ] + }) + } + } + + assert { + condition = length(aws_iam_role_policy.lambda_additional_inline_policies) == 1 + error_message = "Should create one inline policy" + } + + assert { + condition = length(aws_iam_role.role) == 1 + error_message = "Should create IAM role when using inline policies" + } +} + +run "test_lambda_with_managed_policies" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + additional_managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", + "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" + ] + } + + assert { + condition = length(aws_iam_role_policy_attachment.lambda_additional_managed_policies) == 2 + error_message = "Should attach two managed policies" + } + + assert { + condition = length(aws_iam_role.role) == 1 + error_message = "Should create IAM role when using managed policies" + } +} + +run "test_lambda_with_both_policy_types" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + + additional_managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess" + ] + + additional_inline_policies = { + sqs_access = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["sqs:SendMessage"] + Resource = "arn:aws:sqs:us-east-1:123456789012:my-queue" + } + ] + }) + } + } + + assert { + condition = length(aws_iam_role_policy_attachment.lambda_additional_managed_policies) == 1 + error_message = "Should attach one managed policy" + } + + assert { + condition = length(aws_iam_role_policy.lambda_additional_inline_policies) == 1 + error_message = "Should create one inline policy" + } +} + +run "test_lambda_with_function_url_iam_auth" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + enable_function_url = true + function_url_auth_type = "AWS_IAM" + } + + assert { + condition = length(aws_lambda_function_url.function_url) == 1 + error_message = "Should create Function URL when enable_function_url is true" + } + + assert { + condition = aws_lambda_function_url.function_url[0].authorization_type == "AWS_IAM" + error_message = "Function URL auth type should be AWS_IAM" + } +} + +run "test_lambda_with_function_url_no_auth" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "nodejs20.x" + handler = "index.handler" + enable_function_url = true + function_url_auth_type = "NONE" + } + + assert { + condition = length(aws_lambda_function_url.function_url) == 1 + error_message = "Should create Function URL when enable_function_url is true" + } + + assert { + condition = aws_lambda_function_url.function_url[0].authorization_type == "NONE" + error_message = "Function URL auth type should be NONE" + } +} + +run "test_lambda_without_function_url" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + enable_function_url = false + } + + assert { + condition = length(aws_lambda_function_url.function_url) == 0 + error_message = "Should not create Function URL when enable_function_url is false" + } + + assert { + condition = output.function_url == null + error_message = "Function URL output should be null when function URL is not enabled" + } +} + +run "test_lambda_with_function_url_cors" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "nodejs20.x" + handler = "index.handler" + enable_function_url = true + function_url_auth_type = "NONE" + + function_url_cors = { + allow_origins = ["https://example.com"] + allow_methods = ["GET", "POST"] + allow_headers = ["content-type"] + expose_headers = ["x-request-id"] + allow_credentials = true + max_age = 3600 + } + } + + assert { + condition = length(aws_lambda_function_url.function_url) == 1 + error_message = "Should create Function URL with CORS" + } + + assert { + condition = length(aws_lambda_function_url.function_url[0].cors) == 1 + error_message = "Should configure CORS on Function URL" + } +} + +run "test_lambda_go_runtime" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "bootstrap.zip" + runtime = "provided.al2023" + handler = "bootstrap" + architectures = ["x86_64"] + } + + assert { + condition = aws_lambda_function.function.runtime == "provided.al2023" + error_message = "Runtime should be provided.al2023 for Go" + } + + assert { + condition = aws_lambda_function.function.handler == "bootstrap" + error_message = "Handler should be bootstrap for Go" + } +} + +run "test_lambda_java_runtime" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "java-function.zip" + runtime = "java21" + handler = "com.example.Handler::handleRequest" + } + + assert { + condition = aws_lambda_function.function.runtime == "java21" + error_message = "Runtime should be java21" + } +} + +run "test_lambda_custom_name_prefix" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + name_prefix = "custom-prefix-" + } + + # Note: We can't assert on the exact function name due to random suffix, + # but we can validate that the function is created + assert { + condition = aws_lambda_function.function.runtime == "python3.12" + error_message = "Function should be created with custom prefix" + } +} + +run "test_lambda_custom_iam_role_name_prefix" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + iam_role_name_prefix = "custom-role-prefix-" + } + + assert { + condition = length(aws_iam_role.role) == 1 + error_message = "Should create IAM role" + } + + assert { + condition = aws_iam_role.role[0].name_prefix == "custom-role-prefix-" + error_message = "IAM role should use custom name prefix" + } +} + +run "test_outputs" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + } + + # Note: In plan mode, output values that depend on computed resources (like random_id) + # are not available. We validate the resources are created correctly instead. + assert { + condition = aws_lambda_function.function.runtime == "python3.12" + error_message = "Lambda function should be created" + } + + assert { + condition = aws_lambda_function.function.handler == "lambda_function.lambda_handler" + error_message = "Lambda function handler should be set correctly" + } + + assert { + condition = length(aws_iam_role.role) == 1 + error_message = "IAM role should be created for outputs" + } +} + +run "test_s3_access_policy" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + } + + assert { + condition = length(aws_iam_role_policy.s3_zip_bucket_access) == 1 + error_message = "Should create S3 access policy for deployment package" + } +} + +run "test_lambda_basic_execution_role" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + } + + assert { + condition = aws_iam_role_policy_attachment.lambda_basic.policy_arn == "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + error_message = "Should attach AWSLambdaBasicExecutionRole" + } +} + +run "test_package_type" { + command = plan + + variables { + s3_bucket = "test-deployment-bucket" + s3_key = "test-function.zip" + runtime = "python3.12" + handler = "lambda_function.lambda_handler" + } + + assert { + condition = aws_lambda_function.function.package_type == "Zip" + error_message = "Package type should be Zip" + } + + assert { + condition = aws_lambda_function.function.s3_bucket == "test-deployment-bucket" + error_message = "S3 bucket should match the provided value" + } + + assert { + condition = aws_lambda_function.function.s3_key == "test-function.zip" + error_message = "S3 key should match the provided value" + } } diff --git a/variables.tf b/variables.tf index 484f962..adf34d5 100644 --- a/variables.tf +++ b/variables.tf @@ -1 +1,109 @@ -# TF variables go here \ No newline at end of file +variable "s3_bucket" { + description = "The S3 bucket name where the Lambda deployment package (zip) is stored" + type = string +} + +variable "s3_key" { + description = "The S3 object key (path) for the Lambda deployment package" + type = string +} + +variable "handler" { + description = "The function entrypoint in your code (e.g., 'index.handler' for Node.js)" + type = string +} + +variable "runtime" { + description = "The Lambda runtime identifier (e.g., 'python3.12', 'nodejs20.x', 'java21')" + type = string +} + +variable "iam_role_arn" { + description = "Optional IAM role ARN to use for the Lambda function. If not provided, a new role will be created" + type = string + default = null +} + +variable "architectures" { + description = "Instruction set architecture for the Lambda function. Valid values: ['x86_64'] or ['arm64']" + type = list(string) + default = ["x86_64"] +} + +variable "timeout_in_seconds" { + description = "The amount of time the Lambda function has to run in seconds" + type = number + default = 300 +} + +variable "additional_tags" { + description = "Additional tags to apply to the Lambda function" + type = map(string) + default = {} +} + +variable "environment_variables" { + description = "Environment variables to pass to the Lambda function" + type = map(string) + default = {} +} + +variable "memory_size" { + description = "Amount of memory in MB that your Lambda function can use at runtime. Valid value between 128 MB to 10,240 MB" + type = number + default = 128 +} + +variable "name_prefix" { + description = "Prefix for the Lambda function name" + type = string + default = "hum-orch-" +} + +variable "additional_managed_policy_arns" { + description = "List of additional managed IAM policy ARNs to attach to the Lambda execution role" + type = list(string) + default = [] +} + +variable "additional_inline_policies" { + description = "Map of additional inline IAM policies to attach to the Lambda execution role. Key is the policy name, value is the policy document as JSON string" + type = map(string) + default = {} +} + +variable "iam_role_name_prefix" { + description = "Prefix for the IAM role name. Only used when a new IAM role is created (iam_role_arn not provided)" + type = string + default = "lambda-role-" +} + +variable "enable_function_url" { + description = "If true, creates an HTTPS endpoint (Function URL) for the Lambda. Allows HTTP/HTTPS invocation." + type = bool + default = false +} + +variable "function_url_auth_type" { + description = "Authorization type for the Function URL. 'NONE' = public access, 'AWS_IAM' = requires AWS credentials." + type = string + default = "AWS_IAM" + + validation { + condition = contains(["NONE", "AWS_IAM"], var.function_url_auth_type) + error_message = "function_url_auth_type must be either 'NONE' or 'AWS_IAM'." + } +} + +variable "function_url_cors" { + description = "CORS configuration for the Function URL. Only applies if enable_function_url is true." + type = object({ + allow_credentials = optional(bool, false) + allow_origins = optional(list(string), ["*"]) + allow_methods = optional(list(string), ["*"]) + allow_headers = optional(list(string), []) + expose_headers = optional(list(string), []) + max_age = optional(number, 0) + }) + default = null +} diff --git a/versions.tf b/versions.tf index 0f4f39a..b697a2e 100644 --- a/versions.tf +++ b/versions.tf @@ -1,9 +1,14 @@ -# TF version and required providers go here terraform { - # Defining a required TF version is recommended - # required_version = ">= 1.9.0" + required_version = ">= 1.6.0" required_providers { - # ... + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } } -} \ No newline at end of file +} From 54575873e6a88c1429e43bc91a5b68409aa3c33f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 15:01:17 +0000 Subject: [PATCH 2/2] terraform-docs: automated action --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index 4d87645..ff98fb8 100644 --- a/README.md +++ b/README.md @@ -795,3 +795,71 @@ The test suite validates: - [Lambda Function URL Configuration Examples](./USAGE-EXAMPLES.md) - Detailed IAM policy configuration examples - [Test Invocations](./TEST-INVOCATIONS.md) - Manual testing instructions + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.6.0 | +| [aws](#requirement\_aws) | ~> 6.0 | +| [random](#requirement\_random) | ~> 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 6.21.0 | +| [random](#provider\_random) | 3.7.2 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_role.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.lambda_additional_inline_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.s3_zip_bucket_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.lambda_additional_managed_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.lambda_basic](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_function_url.function_url](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url) | resource | +| [random_id.entropy](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_inline\_policies](#input\_additional\_inline\_policies) | Map of additional inline IAM policies to attach to the Lambda execution role. Key is the policy name, value is the policy document as JSON string | `map(string)` | `{}` | no | +| [additional\_managed\_policy\_arns](#input\_additional\_managed\_policy\_arns) | List of additional managed IAM policy ARNs to attach to the Lambda execution role | `list(string)` | `[]` | no | +| [additional\_tags](#input\_additional\_tags) | Additional tags to apply to the Lambda function | `map(string)` | `{}` | no | +| [architectures](#input\_architectures) | Instruction set architecture for the Lambda function. Valid values: ['x86\_64'] or ['arm64'] | `list(string)` |
[
"x86_64"
]
| no | +| [enable\_function\_url](#input\_enable\_function\_url) | If true, creates an HTTPS endpoint (Function URL) for the Lambda. Allows HTTP/HTTPS invocation. | `bool` | `false` | no | +| [environment\_variables](#input\_environment\_variables) | Environment variables to pass to the Lambda function | `map(string)` | `{}` | no | +| [function\_url\_auth\_type](#input\_function\_url\_auth\_type) | Authorization type for the Function URL. 'NONE' = public access, 'AWS\_IAM' = requires AWS credentials. | `string` | `"AWS_IAM"` | no | +| [function\_url\_cors](#input\_function\_url\_cors) | CORS configuration for the Function URL. Only applies if enable\_function\_url is true. |
object({
allow_credentials = optional(bool, false)
allow_origins = optional(list(string), ["*"])
allow_methods = optional(list(string), ["*"])
allow_headers = optional(list(string), [])
expose_headers = optional(list(string), [])
max_age = optional(number, 0)
})
| `null` | no | +| [handler](#input\_handler) | The function entrypoint in your code (e.g., 'index.handler' for Node.js) | `string` | n/a | yes | +| [iam\_role\_arn](#input\_iam\_role\_arn) | Optional IAM role ARN to use for the Lambda function. If not provided, a new role will be created | `string` | `null` | no | +| [iam\_role\_name\_prefix](#input\_iam\_role\_name\_prefix) | Prefix for the IAM role name. Only used when a new IAM role is created (iam\_role\_arn not provided) | `string` | `"lambda-role-"` | no | +| [memory\_size](#input\_memory\_size) | Amount of memory in MB that your Lambda function can use at runtime. Valid value between 128 MB to 10,240 MB | `number` | `128` | no | +| [name\_prefix](#input\_name\_prefix) | Prefix for the Lambda function name | `string` | `"hum-orch-"` | no | +| [runtime](#input\_runtime) | The Lambda runtime identifier (e.g., 'python3.12', 'nodejs20.x', 'java21') | `string` | n/a | yes | +| [s3\_bucket](#input\_s3\_bucket) | The S3 bucket name where the Lambda deployment package (zip) is stored | `string` | n/a | yes | +| [s3\_key](#input\_s3\_key) | The S3 object key (path) for the Lambda deployment package | `string` | n/a | yes | +| [timeout\_in\_seconds](#input\_timeout\_in\_seconds) | The amount of time the Lambda function has to run in seconds | `number` | `300` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [function\_arn](#output\_function\_arn) | The ARN of the Lambda function | +| [function\_name](#output\_function\_name) | The name of the Lambda function | +| [function\_url](#output\_function\_url) | The HTTPS URL endpoint for the Lambda function (if enable\_function\_url is true) | +| [humanitec\_metadata](#output\_humanitec\_metadata) | The Humanitec metadata annotations for the Lambda function | +| [invoke\_arn](#output\_invoke\_arn) | The ARN to be used for invoking the Lambda function from API Gateway | +| [role\_arn](#output\_role\_arn) | The ARN of the IAM role used by the Lambda function | + \ No newline at end of file