From 4136f13cd0c72b714f64143afb65fc363f929ca5 Mon Sep 17 00:00:00 2001 From: Matt Boland Date: Mon, 29 Dec 2025 15:18:28 -0600 Subject: [PATCH] feat: added pattern for lambda managed instances using terraform --- lambda-managed-instances-tf/.gitignore | 55 ++ lambda-managed-instances-tf/README.md | 360 ++++++++++++ .../events/hello-world.json | 3 + .../example-pattern.json | 72 +++ .../lambda/hello-world.py | 30 + lambda-managed-instances-tf/main.tf | 515 ++++++++++++++++++ lambda-managed-instances-tf/outputs.tf | 76 +++ .../terraform.tfvars.example | 11 + lambda-managed-instances-tf/test-lambda.sh | 121 ++++ lambda-managed-instances-tf/variables.tf | 19 + 10 files changed, 1262 insertions(+) create mode 100644 lambda-managed-instances-tf/.gitignore create mode 100644 lambda-managed-instances-tf/README.md create mode 100644 lambda-managed-instances-tf/events/hello-world.json create mode 100644 lambda-managed-instances-tf/example-pattern.json create mode 100644 lambda-managed-instances-tf/lambda/hello-world.py create mode 100644 lambda-managed-instances-tf/main.tf create mode 100644 lambda-managed-instances-tf/outputs.tf create mode 100644 lambda-managed-instances-tf/terraform.tfvars.example create mode 100755 lambda-managed-instances-tf/test-lambda.sh create mode 100644 lambda-managed-instances-tf/variables.tf diff --git a/lambda-managed-instances-tf/.gitignore b/lambda-managed-instances-tf/.gitignore new file mode 100644 index 000000000..9a9a64177 --- /dev/null +++ b/lambda-managed-instances-tf/.gitignore @@ -0,0 +1,55 @@ +# Terraform files +*.tfstate +*.tfstate.* +*.tfvars +*.tfvars.json +.terraform/ +.terraform.lock.hcl +terraform.tfplan +terraform.tfplan.json + +# Lambda deployment package +lambda-function.zip + +# Test response files +response.json +custom-response.json +output.json + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg \ No newline at end of file diff --git a/lambda-managed-instances-tf/README.md b/lambda-managed-instances-tf/README.md new file mode 100644 index 000000000..1fbab0e48 --- /dev/null +++ b/lambda-managed-instances-tf/README.md @@ -0,0 +1,360 @@ +# Lambda Hello World on Lambda Managed Instances (Terraform) + +This pattern demonstrates how to deploy a simple Hello World Lambda function running on Lambda Managed Instances using Terraform. Lambda Managed Instances provide predictable performance and reduced cold starts for your Lambda functions. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-managed-instances-tf + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +**Note**: Lambda Managed Instances provision EC2 instances that are **NOT eligible for the AWS Free Tier**. These instances will incur charges immediately upon deployment, regardless of your Free Tier status. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) (latest available version) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Terraform](https://www.terraform.io/downloads.html) (version 1.0 or later) installed and configured +* [Python](https://www.python.org/) (version 3.13 or later) for the Lambda function runtime + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd lambda-managed-instances-tf + ``` +1. Initialize Terraform: + ``` + terraform init + ``` +1. Review the Terraform plan: + ``` + terraform plan + ``` +1. Deploy the Terraform stack: + ``` + terraform apply + ``` + Note: This stack will deploy to your default AWS region (us-east-1). You can change the region by modifying the `aws_region` variable in `variables.tf` or by passing `-var="aws_region=your-region"` to the terraform commands. +1. Note the outputs from the Terraform deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +This pattern demonstrates the deployment of a simple Lambda function on Lambda Managed Instances: + +### Lambda Managed Instances + +[Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html) provide: +- Predictable performance with pre-warmed execution environments +- Reduced cold start latency +- Consistent execution characteristics +- Better resource utilization for frequently invoked functions + +The underlying EC2 infrastructure can be inspected using AWS CLI commands to understand how managed instances work (see "Inspecting Lambda Managed Instances Infrastructure" section below). + +### Hello World Function + +The Lambda function is a simple Hello World implementation that: +- Accepts an event with a name parameter +- Returns a JSON response with a greeting message +- Uses Python's standard logging library for event logging +- Demonstrates minimal Lambda function structure using Python type hints + +### CloudWatch Log Group + +The pattern includes a dedicated CloudWatch log group with: +- **Custom log group name**: `/demo/lambda/hello-world-managed-instances-tf` +- **Retention period**: 2 weeks (14 days) to manage storage costs +- **Automatic cleanup**: Resources are destroyed when `terraform destroy` is run +- **Direct integration**: The Lambda function is configured to use this specific log group + +### VPC Configuration + +The Terraform implementation creates a complete VPC setup including: +- **VPC**: Custom VPC with DNS support (10.0.0.0/16 CIDR) +- **Subnets**: 3 public and 3 private subnets across 3 availability zones for high availability + - Public subnets: 10.0.0.0/19, 10.0.32.0/19, 10.0.64.0/19 + - Private subnets: 10.0.96.0/19, 10.0.128.0/19, 10.0.160.0/19 +- **NAT Gateways**: 3 NAT Gateways (one per AZ) for outbound internet access from private subnets +- **Security Groups**: Configured for Lambda function access with restricted default security group +- **Route Tables**: Separate route tables for each subnet with proper routing configuration + +## Testing + +After deployment, you can test the Lambda function using AWS CLI or AWS Console. + +### AWS CLI Testing + +1. **Basic function invocation**: + ```bash + aws lambda invoke \ + --function-name hello-world-managed-instances-tf:live \ + --payload file://events/hello-world.json \ + --cli-binary-format raw-in-base64-out \ + response.json + ``` + +2. **View the response**: + ```bash + cat response.json + ``` + +3. **Custom name invocation**: + ```bash + echo '{"name":"Lambda Managed Instances"}' | aws lambda invoke \ + --function-name hello-world-managed-instances-tf:live \ + --payload file:///dev/stdin \ + --cli-binary-format raw-in-base64-out \ + custom-response.json + ``` + +4. **View CloudWatch logs**: + ```bash + aws logs filter-log-events \ + --log-group-name /demo/lambda/hello-world-managed-instances-tf \ + --start-time $(date -d '5 minutes ago' +%s)000 + ``` + +5. **View Terraform outputs**: + ```bash + terraform output + ``` + + **Available outputs:** + - `function_name` - Lambda function name for CLI invocation + - `function_arn` - Lambda function ARN + - `function_alias` - Lambda function alias for invocation (function:live) + - `log_group_name` - CloudWatch Log Group name + - `capacity_provider_arn` - Lambda Capacity Provider ARN + - `capacity_provider_name` - Lambda Capacity Provider name + - `vpc_id` - VPC ID for Lambda Managed Instances + - `private_subnet_ids` - Private subnet IDs (3 subnets across AZs) + - `public_subnet_ids` - Public subnet IDs (3 subnets across AZs) + - `security_group_id` - Lambda Security Group ID + - `default_security_group_id` - Default Security Group ID (restricted) + - `nat_gateway_ids` - NAT Gateway IDs (3 gateways) + - `elastic_ip_addresses` - Elastic IP addresses for NAT Gateways + +### Automated Testing Script + +Use the included test script for comprehensive testing: + +```bash +./test-lambda.sh [aws-profile] +``` + +This script performs: +- Function invocation with sample events +- CloudWatch logs inspection +- Capacity provider details retrieval +- EC2 instances inspection for managed instances + +**Using Terraform outputs in the test script:** +```bash +# Get function name from Terraform output +FUNCTION_NAME=$(terraform output -raw function_name) + +# Invoke function using output +aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload file://events/hello-world.json \ + response.json + +# View logs using output +aws logs filter-log-events \ + --log-group-name $(terraform output -raw log_group_name) \ + --start-time $(date -d '5 minutes ago' +%s)000 +``` + +### AWS Console Testing + +1. Navigate to the Lambda service in the AWS Console +2. Find the function named `hello-world-managed-instances-tf` +3. Create a test event using the payload from `events/hello-world.json` or create a custom payload: + ```json + { + "name": "Your Custom Name" + } + ``` +4. Execute the test and observe the results in the execution logs + +### Expected Response + +The function returns a JSON response with the following structure: + +```json +{ + "response": "Hello AWS Lambda on Managed Instances" +} +``` + +### Monitoring and Observability + +Monitor the function execution through: +- **CloudWatch Logs**: Detailed execution logs with event and response data in the dedicated log group +- **Lambda Metrics**: Function performance and invocation statistics +- **CloudWatch Metrics**: Custom metrics and alarms for monitoring + +The Terraform outputs include all resource identifiers for easy reference when setting up monitoring dashboards or log analysis tools: + +```bash +# Monitor function execution +aws logs filter-log-events \ + --log-group-name $(terraform output -raw log_group_name) \ + --start-time $(date -d '1 hour ago' +%s)000 + +# Check VPC and networking +aws ec2 describe-vpc --vpc-ids $(terraform output -raw vpc_id) +aws ec2 describe-subnets --subnet-ids $(terraform output -json private_subnet_ids | jq -r '.[]') +aws ec2 describe-nat-gateways --nat-gateway-ids $(terraform output -json nat_gateway_ids | jq -r '.[]') + +# Inspect capacity provider +aws lambda get-capacity-provider --capacity-provider-name $(terraform output -raw capacity_provider_name) +``` + +## Inspecting Lambda Managed Instances Infrastructure + +Lambda Managed Instances provision EC2 instances behind the scenes to provide predictable performance. You can inspect this infrastructure using AWS CLI commands: + +### View Capacity Provider Details + +```bash +# Using Terraform output +aws lambda get-capacity-provider --capacity-provider-name $(terraform output -raw capacity_provider_name) +``` + +This shows: +- Capacity provider ARN and state +- VPC configuration (subnets and security groups) +- Instance requirements (architecture, scaling mode) +- IAM roles and permissions + +### List Associated EC2 Instances + +```bash +# Using Terraform output for capacity provider ARN +CAPACITY_PROVIDER_ARN=$(terraform output -raw capacity_provider_arn) +aws ec2 describe-instances \ + --filters "Name=tag:aws:lambda:capacity-provider,Values=$CAPACITY_PROVIDER_ARN" \ + --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name,LaunchTime,SubnetId,PrivateIpAddress]' \ + --output table +``` + +This displays: +- Instance IDs and types +- Current state (running, pending, terminated) +- Launch times and subnet distribution +- Private IP addresses within the VPC + +**Note**: For a complete list of supported EC2 instance types for Lambda Managed Instances and their pricing, see the [AWS Lambda Pricing page](https://aws.amazon.com/lambda/pricing/). + +### Understanding Instance Behavior + +**Auto-scaling**: Instances are automatically created and terminated based on function demand +- **Scale-up**: New instances launch when function invocation increases +- **Scale-down**: Unused instances terminate after periods of low activity +- **Multi-AZ**: Instances are distributed across availability zones for high availability + +**Instance Lifecycle**: +- Instances typically launch within 1-2 minutes of stack deployment +- They remain running to provide immediate function execution +- AWS manages all instance lifecycle operations automatically + +### Automated Testing + +The included test script (`./test-lambda.sh`) automatically inspects both the capacity provider and EC2 instances, providing a comprehensive view of the managed instances infrastructure. + +## Terraform Configuration + +### Variables + +The following variables can be customized in `variables.tf` or passed via command line: + +- `aws_region`: AWS region for deployment (default: us-east-1) +- `environment`: Environment name for tagging (default: demo) +- `project_name`: Project name for resource naming (default: lambda-managed-instances) + +### Outputs + +The Terraform configuration provides the following outputs: + +- `function_name`: Lambda function name +- `function_arn`: Lambda function ARN +- `function_alias`: Lambda function alias (function:live) +- `log_group_name`: CloudWatch log group name +- `capacity_provider_arn`: Lambda capacity provider ARN +- `capacity_provider_name`: Lambda capacity provider name +- `vpc_id`: VPC ID +- `private_subnet_ids`: Private subnet IDs (array of 3) +- `public_subnet_ids`: Public subnet IDs (array of 3) +- `security_group_id`: Lambda security group ID +- `default_security_group_id`: Default security group ID (restricted) +- `nat_gateway_ids`: NAT Gateway IDs (array of 3) +- `elastic_ip_addresses`: Elastic IP addresses (array of 3) + +### Using Outputs in Scripts + +```bash +# Get all outputs +terraform output + +# Get specific output values +FUNCTION_NAME=$(terraform output -raw function_name) +VPC_ID=$(terraform output -raw vpc_id) +CAPACITY_PROVIDER_ARN=$(terraform output -raw capacity_provider_arn) + +# Use in AWS CLI commands +aws lambda invoke --function-name "$FUNCTION_NAME" --payload '{}' response.json +aws ec2 describe-vpc --vpc-ids "$VPC_ID" + +# Use function alias for managed instances +FUNCTION_ALIAS=$(terraform output -raw function_alias) +aws lambda invoke --function-name "$FUNCTION_ALIAS" --payload '{}' response.json + +# Get capacity provider association status +terraform output capacity_provider_association + +# Get manual association command if needed +terraform output manual_association_command +``` + +## Regional Availability + +This stack will deploy to your configured AWS region (default: us-east-1). Before deploying, please verify that Lambda Managed Instances feature is available in your target region by using the [AWS capabilities explorer](https://builder.aws.com/build/capabilities/explore) or consulting the official [Lambda Managed Instances documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html). + +## Cleanup + +1. Delete the Terraform stack: + ```bash + terraform destroy + ``` +1. Confirm all resources have been deleted by checking the AWS Console or running: + ```bash + terraform show + ``` + +## File Structure + +``` +lambda-managed-instances-tf/ +├── main.tf # Main Terraform configuration +├── variables.tf # Input variables +├── outputs.tf # Output values +├── README.md # This file +├── test-lambda.sh # Testing script +├── .gitignore # Terraform-specific gitignore +├── terraform.tfvars.example # Example variables file +├── example-pattern.json # Pattern metadata +├── lambda/ +│ └── hello-world.py # Lambda function code +└── events/ + └── hello-world.json # Test event payload +``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/lambda-managed-instances-tf/events/hello-world.json b/lambda-managed-instances-tf/events/hello-world.json new file mode 100644 index 000000000..3d0c51d83 --- /dev/null +++ b/lambda-managed-instances-tf/events/hello-world.json @@ -0,0 +1,3 @@ +{ + "name": "AWS Lambda on Managed Instances" +} \ No newline at end of file diff --git a/lambda-managed-instances-tf/example-pattern.json b/lambda-managed-instances-tf/example-pattern.json new file mode 100644 index 000000000..ffeda810c --- /dev/null +++ b/lambda-managed-instances-tf/example-pattern.json @@ -0,0 +1,72 @@ +{ + "title": "Lambda Hello World on Lambda Managed Instances (Terraform)", + "description": "Deploy a simple Hello World Lambda function on Lambda Managed Instances using Terraform", + "language": "Python", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to deploy a simple Hello World Lambda function running on Lambda Managed Instances using Terraform.", + "Lambda Managed Instances provide predictable performance and reduced cold starts for your Lambda functions by pre-warming execution environments.", + "The Hello World function accepts an event with a name parameter and returns a simple JSON response with a greeting message.", + "The Terraform implementation includes a complete VPC setup with private subnets, NAT gateways, and proper security groups for Lambda Managed Instances." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-managed-instances-tf", + "templateURL": "serverless-patterns/lambda-managed-instances-tf", + "projectFolder": "lambda-managed-instances-tf", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Managed Instances documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html" + }, + { + "text": "AWS Lambda Pricing (supported instance types)", + "link": "https://aws.amazon.com/lambda/pricing/" + }, + { + "text": "AWS Lambda Developer Guide", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/" + }, + { + "text": "Terraform AWS Provider Documentation", + "link": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs" + }, + { + "text": "Terraform Lambda Function Resource", + "link": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform plan", + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: terraform destroy." + ] + }, + "authors": [ + { + "name": "Matt Boland", + "bio": "Sr. Solutions Architect, AWS.", + "linkedin": "matt-boland" + } + ] +} \ No newline at end of file diff --git a/lambda-managed-instances-tf/lambda/hello-world.py b/lambda-managed-instances-tf/lambda/hello-world.py new file mode 100644 index 000000000..5027d6169 --- /dev/null +++ b/lambda-managed-instances-tf/lambda/hello-world.py @@ -0,0 +1,30 @@ +import json +import logging +from typing import Dict, Any + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]: + """ + Lambda function handler that returns a greeting message. + + Args: + event: Lambda event containing optional 'name' field + context: Lambda context object + + Returns: + Dictionary with greeting response + """ + logger.info(f"Processing event: {json.dumps(event)}") + + name = event.get('name', 'World') + + response = { + 'response': f'Hello {name}' + } + + logger.info(f"Returning response: {json.dumps(response)}") + + return response \ No newline at end of file diff --git a/lambda-managed-instances-tf/main.tf b/lambda-managed-instances-tf/main.tf new file mode 100644 index 000000000..78279780d --- /dev/null +++ b/lambda-managed-instances-tf/main.tf @@ -0,0 +1,515 @@ +# Lambda Hello World on Lambda Managed Instances - Terraform Implementation + +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.25" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.4" + } + null = { + source = "hashicorp/null" + version = "~> 3.2" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# Local variables +locals { + function_name = "hello-world-managed-instances-tf" + log_group_name = "/demo/lambda/${local.function_name}" +} + +# Data source to get current AWS account and region +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +# Create ZIP archive of Lambda function +data "archive_file" "lambda_zip" { + type = "zip" + source_dir = "${path.module}/lambda" + output_path = "${path.module}/lambda-function.zip" +} + +# CloudWatch Log Group +resource "aws_cloudwatch_log_group" "demo_log_group" { + name = local.log_group_name + retention_in_days = 14 + + tags = { + Name = "DemoLogGroup" + Environment = "demo" + } +} + +# IAM role for Lambda function +resource "aws_iam_role" "lambda_role" { + name = "${local.function_name}-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${local.function_name}-role" + Environment = "demo" + } +} + +# IAM policy attachment for basic Lambda execution +resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.lambda_role.name +} + +# VPC for Lambda Managed Instances +resource "aws_vpc" "lambda_managed_instances_vpc" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "LambdaManagedInstancesVPC" + Environment = "demo" + } +} + +# Internet Gateway +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + tags = { + Name = "LambdaManagedInstancesIGW" + Environment = "demo" + } +} + +# Public subnets (matching CDK CIDR blocks) +resource "aws_subnet" "public_subnet_1" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.0.0/19" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "LambdaManagedInstancesPublicSubnet1" + Environment = "demo" + "aws-cdk:subnet-name" = "Public" + "aws-cdk:subnet-type" = "Public" + } +} + +resource "aws_subnet" "public_subnet_2" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.32.0/19" + availability_zone = data.aws_availability_zones.available.names[1] + map_public_ip_on_launch = true + + tags = { + Name = "LambdaManagedInstancesPublicSubnet2" + Environment = "demo" + "aws-cdk:subnet-name" = "Public" + "aws-cdk:subnet-type" = "Public" + } +} + +resource "aws_subnet" "public_subnet_3" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.64.0/19" + availability_zone = data.aws_availability_zones.available.names[2] + map_public_ip_on_launch = true + + tags = { + Name = "LambdaManagedInstancesPublicSubnet3" + Environment = "demo" + "aws-cdk:subnet-name" = "Public" + "aws-cdk:subnet-type" = "Public" + } +} + +# Private subnets (matching CDK CIDR blocks) +resource "aws_subnet" "private_subnet_1" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.96.0/19" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "LambdaManagedInstancesPrivateSubnet1" + Environment = "demo" + "aws-cdk:subnet-name" = "Private" + "aws-cdk:subnet-type" = "Private" + } +} + +resource "aws_subnet" "private_subnet_2" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.128.0/19" + availability_zone = data.aws_availability_zones.available.names[1] + + tags = { + Name = "LambdaManagedInstancesPrivateSubnet2" + Environment = "demo" + "aws-cdk:subnet-name" = "Private" + "aws-cdk:subnet-type" = "Private" + } +} + +resource "aws_subnet" "private_subnet_3" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + cidr_block = "10.0.160.0/19" + availability_zone = data.aws_availability_zones.available.names[2] + + tags = { + Name = "LambdaManagedInstancesPrivateSubnet3" + Environment = "demo" + "aws-cdk:subnet-name" = "Private" + "aws-cdk:subnet-type" = "Private" + } +} + +# Data source for availability zones +data "aws_availability_zones" "available" { + state = "available" +} + +# Elastic IPs for NAT Gateways +resource "aws_eip" "nat_eip_1" { + domain = "vpc" + depends_on = [aws_internet_gateway.igw] + + tags = { + Name = "LambdaManagedInstancesNATEIP1" + Environment = "demo" + } +} + +resource "aws_eip" "nat_eip_2" { + domain = "vpc" + depends_on = [aws_internet_gateway.igw] + + tags = { + Name = "LambdaManagedInstancesNATEIP2" + Environment = "demo" + } +} + +resource "aws_eip" "nat_eip_3" { + domain = "vpc" + depends_on = [aws_internet_gateway.igw] + + tags = { + Name = "LambdaManagedInstancesNATEIP3" + Environment = "demo" + } +} + +# NAT Gateways +resource "aws_nat_gateway" "nat_gateway_1" { + allocation_id = aws_eip.nat_eip_1.id + subnet_id = aws_subnet.public_subnet_1.id + + tags = { + Name = "LambdaManagedInstancesNATGateway1" + Environment = "demo" + } + + depends_on = [aws_internet_gateway.igw] +} + +resource "aws_nat_gateway" "nat_gateway_2" { + allocation_id = aws_eip.nat_eip_2.id + subnet_id = aws_subnet.public_subnet_2.id + + tags = { + Name = "LambdaManagedInstancesNATGateway2" + Environment = "demo" + } + + depends_on = [aws_internet_gateway.igw] +} + +resource "aws_nat_gateway" "nat_gateway_3" { + allocation_id = aws_eip.nat_eip_3.id + subnet_id = aws_subnet.public_subnet_3.id + + tags = { + Name = "LambdaManagedInstancesNATGateway3" + Environment = "demo" + } + + depends_on = [aws_internet_gateway.igw] +} + +# Route table for public subnets +resource "aws_route_table" "public_route_table_1" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "LambdaManagedInstancesPublicRouteTable1" + Environment = "demo" + } +} + +resource "aws_route_table" "public_route_table_2" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "LambdaManagedInstancesPublicRouteTable2" + Environment = "demo" + } +} + +resource "aws_route_table" "public_route_table_3" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "LambdaManagedInstancesPublicRouteTable3" + Environment = "demo" + } +} + +# Route table associations for public subnets +resource "aws_route_table_association" "public_subnet_1_association" { + subnet_id = aws_subnet.public_subnet_1.id + route_table_id = aws_route_table.public_route_table_1.id +} + +resource "aws_route_table_association" "public_subnet_2_association" { + subnet_id = aws_subnet.public_subnet_2.id + route_table_id = aws_route_table.public_route_table_2.id +} + +resource "aws_route_table_association" "public_subnet_3_association" { + subnet_id = aws_subnet.public_subnet_3.id + route_table_id = aws_route_table.public_route_table_3.id +} + +# Route tables for private subnets +resource "aws_route_table" "private_route_table_1" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat_gateway_1.id + } + + tags = { + Name = "LambdaManagedInstancesPrivateRouteTable1" + Environment = "demo" + } +} + +resource "aws_route_table" "private_route_table_2" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat_gateway_2.id + } + + tags = { + Name = "LambdaManagedInstancesPrivateRouteTable2" + Environment = "demo" + } +} + +resource "aws_route_table" "private_route_table_3" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat_gateway_3.id + } + + tags = { + Name = "LambdaManagedInstancesPrivateRouteTable3" + Environment = "demo" + } +} + +# Route table associations for private subnets +resource "aws_route_table_association" "private_subnet_1_association" { + subnet_id = aws_subnet.private_subnet_1.id + route_table_id = aws_route_table.private_route_table_1.id +} + +resource "aws_route_table_association" "private_subnet_2_association" { + subnet_id = aws_subnet.private_subnet_2.id + route_table_id = aws_route_table.private_route_table_2.id +} + +resource "aws_route_table_association" "private_subnet_3_association" { + subnet_id = aws_subnet.private_subnet_3.id + route_table_id = aws_route_table.private_route_table_3.id +} + +# Security Group for Lambda Managed Instances +resource "aws_security_group" "lambda_security_group" { + name_prefix = "${local.function_name}-sg" + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + description = "Allow all outbound traffic by default" + } + + tags = { + Name = "LambdaManagedInstancesSecurityGroup" + Environment = "demo" + } +} + +# Restrict default security group (matching CDK behavior) +resource "aws_default_security_group" "default" { + vpc_id = aws_vpc.lambda_managed_instances_vpc.id + + # Remove all ingress and egress rules + ingress = [] + egress = [] + + tags = { + Name = "LambdaManagedInstancesDefaultSecurityGroup" + Environment = "demo" + } +} + +# Lambda function +resource "aws_lambda_function" "hello_world_function" { + filename = data.archive_file.lambda_zip.output_path + function_name = local.function_name + role = aws_iam_role.lambda_role.arn + handler = "hello-world.lambda_handler" + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + runtime = "python3.13" + architectures = ["arm64"] + description = "Simple Hello World Lambda function on Managed Instances" + memory_size = 2048 + publish = true + + logging_config { + log_format = "JSON" + log_group = aws_cloudwatch_log_group.demo_log_group.name + } + + # Lambda Managed Instances configuration + capacity_provider_config { + lambda_managed_instances_capacity_provider_config { + capacity_provider_arn = aws_lambda_capacity_provider.lambda_capacity_provider.arn + } + } + + # Force recreation when capacity provider changes + lifecycle { + replace_triggered_by = [ + aws_lambda_capacity_provider.lambda_capacity_provider + ] + } + + depends_on = [ + aws_iam_role_policy_attachment.lambda_basic_execution, + aws_cloudwatch_log_group.demo_log_group, + aws_lambda_capacity_provider.lambda_capacity_provider, + ] + + tags = { + Name = local.function_name + Environment = "demo" + } +} + +# Lambda alias for managed instances (required for invocation) +resource "aws_lambda_alias" "hello_world_alias" { + name = "live" + description = "Alias for Lambda Managed Instances function" + function_name = aws_lambda_function.hello_world_function.function_name + function_version = aws_lambda_function.hello_world_function.version +} + +# IAM role for Lambda Capacity Provider +resource "aws_iam_role" "capacity_provider_role" { + name = "${local.function_name}-capacity-provider-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${local.function_name}-capacity-provider-role" + Environment = "demo" + } +} + +# Attach AWS managed policy for Lambda Managed EC2 Resource Operator +resource "aws_iam_role_policy_attachment" "capacity_provider_managed_policy" { + policy_arn = "arn:aws:iam::aws:policy/AWSLambdaManagedEC2ResourceOperator" + role = aws_iam_role.capacity_provider_role.name +} + +# Lambda Capacity Provider for Managed Instances +resource "aws_lambda_capacity_provider" "lambda_capacity_provider" { + name = "lambda-capacity-provider-tf" + + vpc_config { + subnet_ids = [aws_subnet.private_subnet_1.id, aws_subnet.private_subnet_2.id, aws_subnet.private_subnet_3.id] + security_group_ids = [aws_security_group.lambda_security_group.id] + } + + instance_requirements { + architectures = ["arm64"] + } + + permissions_config { + capacity_provider_operator_role_arn = aws_iam_role.capacity_provider_role.arn + } + + tags = { + Name = "lambda-capacity-provider-tf" + Environment = "demo" + } +} + +# Function association with capacity provider is configured in the Lambda function resource above \ No newline at end of file diff --git a/lambda-managed-instances-tf/outputs.tf b/lambda-managed-instances-tf/outputs.tf new file mode 100644 index 000000000..fff3ae639 --- /dev/null +++ b/lambda-managed-instances-tf/outputs.tf @@ -0,0 +1,76 @@ +# Outputs for Lambda Managed Instances Terraform implementation + +output "function_name" { + description = "Lambda function name for CLI invocation" + value = aws_lambda_function.hello_world_function.function_name +} + +output "function_arn" { + description = "Lambda function ARN" + value = aws_lambda_function.hello_world_function.arn +} + +output "log_group_name" { + description = "CloudWatch Log Group name" + value = aws_cloudwatch_log_group.demo_log_group.name +} + +output "capacity_provider_arn" { + description = "Lambda Capacity Provider ARN" + value = aws_lambda_capacity_provider.lambda_capacity_provider.arn +} + +output "capacity_provider_name" { + description = "Lambda Capacity Provider name" + value = aws_lambda_capacity_provider.lambda_capacity_provider.name +} + +output "vpc_id" { + description = "VPC ID for Lambda Managed Instances" + value = aws_vpc.lambda_managed_instances_vpc.id +} + +output "private_subnet_ids" { + description = "Private subnet IDs" + value = [aws_subnet.private_subnet_1.id, aws_subnet.private_subnet_2.id, aws_subnet.private_subnet_3.id] +} + +output "public_subnet_ids" { + description = "Public subnet IDs" + value = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id, aws_subnet.public_subnet_3.id] +} + +output "security_group_id" { + description = "Security Group ID" + value = aws_security_group.lambda_security_group.id +} + +output "default_security_group_id" { + description = "Default Security Group ID (restricted)" + value = aws_default_security_group.default.id +} + +output "capacity_provider_association" { + description = "Confirmation that Lambda function is associated with capacity provider" + value = "Function ${aws_lambda_function.hello_world_function.function_name} is configured to use capacity provider ${aws_lambda_capacity_provider.lambda_capacity_provider.name}" +} + +output "function_alias" { + description = "Lambda function alias for invocation" + value = "${aws_lambda_function.hello_world_function.function_name}:${aws_lambda_alias.hello_world_alias.name}" +} + +output "manual_association_command" { + description = "Manual command to associate Lambda function with capacity provider" + value = "aws lambda put-capacity-provider-function --capacity-provider-arn ${aws_lambda_capacity_provider.lambda_capacity_provider.arn} --function-name ${aws_lambda_function.hello_world_function.function_name}" +} + +output "nat_gateway_ids" { + description = "NAT Gateway IDs" + value = [aws_nat_gateway.nat_gateway_1.id, aws_nat_gateway.nat_gateway_2.id, aws_nat_gateway.nat_gateway_3.id] +} + +output "elastic_ip_addresses" { + description = "Elastic IP addresses for NAT Gateways" + value = [aws_eip.nat_eip_1.public_ip, aws_eip.nat_eip_2.public_ip, aws_eip.nat_eip_3.public_ip] +} \ No newline at end of file diff --git a/lambda-managed-instances-tf/terraform.tfvars.example b/lambda-managed-instances-tf/terraform.tfvars.example new file mode 100644 index 000000000..97eff56f3 --- /dev/null +++ b/lambda-managed-instances-tf/terraform.tfvars.example @@ -0,0 +1,11 @@ +# Example Terraform variables file +# Copy this file to terraform.tfvars and customize as needed + +# AWS region for deployment +aws_region = "us-east-1" + +# Environment name for tagging +environment = "demo" + +# Project name for resource naming +project_name = "lambda-managed-instances" \ No newline at end of file diff --git a/lambda-managed-instances-tf/test-lambda.sh b/lambda-managed-instances-tf/test-lambda.sh new file mode 100755 index 000000000..1ddaf0f3e --- /dev/null +++ b/lambda-managed-instances-tf/test-lambda.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Test script for Hello World Lambda function on Managed Instances (Terraform) +# Usage: ./test-lambda.sh [profile] + +set -e + +# Configuration +FUNCTION_NAME="hello-world-managed-instances-tf" +PROFILE=${1:-default} +EVENT_FILE="events/hello-world.json" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Testing Hello World Lambda Function on Managed Instances (Terraform) ===${NC}" +echo -e "${YELLOW}Function: ${FUNCTION_NAME}${NC}" +echo -e "${YELLOW}Profile: ${PROFILE}${NC}" +echo "" + +# Check if event file exists +if [ ! -f "$EVENT_FILE" ]; then + echo -e "${RED}Error: Event file $EVENT_FILE not found${NC}" + exit 1 +fi + +# Test 1: Basic invocation with sample event +echo -e "${BLUE}Test 1: Basic invocation with sample event${NC}" +echo "Invoking function with event from $EVENT_FILE..." + +aws lambda invoke \ + --function-name "$FUNCTION_NAME:live" \ + --payload file://"$EVENT_FILE" \ + --cli-binary-format raw-in-base64-out \ + --profile "$PROFILE" \ + response.json + +if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Function invoked successfully${NC}" + echo -e "${YELLOW}Response:${NC}" + cat response.json | jq '.' + echo "" +else + echo -e "${RED}✗ Function invocation failed${NC}" + exit 1 +fi + +# Test 2: View recent CloudWatch logs +echo -e "${BLUE}Test 2: Recent CloudWatch logs${NC}" +echo "Fetching recent logs from CloudWatch..." + +LOG_GROUP="/demo/lambda/$FUNCTION_NAME" +START_TIME=$(date -v-5M +%s)000 + +aws logs filter-log-events \ + --log-group-name "$LOG_GROUP" \ + --start-time "$START_TIME" \ + --profile "$PROFILE" \ + --query 'events[*].[timestamp,message]' \ + --output table + +# Test 3: View Lambda Managed Instances (EC2 instances) +echo -e "${BLUE}Test 3: Lambda Managed Instances (EC2 instances)${NC}" +echo "Checking capacity provider and associated EC2 instances..." + +echo -e "${YELLOW}Capacity Provider Details:${NC}" +aws lambda get-capacity-provider --capacity-provider-name lambda-capacity-provider-tf --query 'CapacityProvider.[CapacityProviderArn,State,InstanceRequirements.Architectures[0],CapacityProviderScalingConfig.ScalingMode]' --output table --profile "$PROFILE" + +echo -e "${YELLOW}EC2 Instances provisioned for Lambda Managed Instances:${NC}" +# Get subnet IDs from capacity provider +SUBNET_IDS=$(aws lambda get-capacity-provider --capacity-provider-name lambda-capacity-provider-tf --query 'CapacityProvider.VpcConfig.SubnetIds' --output text --profile "$PROFILE" | tr '\t' ',') +SECURITY_GROUP_ID=$(aws lambda get-capacity-provider --capacity-provider-name lambda-capacity-provider-tf --query 'CapacityProvider.VpcConfig.SecurityGroupIds[0]' --output text --profile "$PROFILE") + +# List EC2 instances tagged with this capacity provider +CAPACITY_PROVIDER_ARN=$(aws lambda get-capacity-provider --capacity-provider-name lambda-capacity-provider-tf --query 'CapacityProvider.CapacityProviderArn' --output text --profile "$PROFILE") +aws ec2 describe-instances \ + --filters "Name=tag:aws:lambda:capacity-provider,Values=$CAPACITY_PROVIDER_ARN" \ + --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name,LaunchTime,SubnetId,PrivateIpAddress]' \ + --output table \ + --profile "$PROFILE" + +# Also show instance count +INSTANCE_COUNT=$(aws ec2 describe-instances \ + --filters "Name=tag:aws:lambda:capacity-provider,Values=$CAPACITY_PROVIDER_ARN" "Name=instance-state-name,Values=running" \ + --query 'length(Reservations[*].Instances[*])' \ + --output text \ + --profile "$PROFILE") + +echo "Currently running instances: $INSTANCE_COUNT" + +echo "" +echo -e "${GREEN}=== Testing completed successfully! ===${NC}" +echo "" +echo -e "${YELLOW}Useful commands for further testing:${NC}" +echo "1. View function details:" +echo " aws lambda get-function --function-name $FUNCTION_NAME --profile $PROFILE" +echo "" +echo "2. View function configuration:" +echo " aws lambda get-function-configuration --function-name $FUNCTION_NAME --profile $PROFILE" +echo "" +echo "3. View CloudWatch logs:" +echo " aws logs filter-log-events --log-group-name $LOG_GROUP --start-time \$(date -d '10 minutes ago' +%s)000 --profile $PROFILE" +echo "" +echo "4. Custom invocation:" +echo " echo '{\"name\":\"Your Name\"}' | aws lambda invoke --function-name $FUNCTION_NAME:live --payload file:///dev/stdin --cli-binary-format raw-in-base64-out --profile $PROFILE output.json" +echo "" +echo "5. View capacity provider details:" +echo " aws lambda get-capacity-provider --capacity-provider-name lambda-capacity-provider-tf --profile $PROFILE" +echo "" +echo "6. List EC2 instances for managed instances:" +echo " aws ec2 describe-instances --filters \"Name=tag:aws:lambda:capacity-provider,Values=$CAPACITY_PROVIDER_ARN\" --profile $PROFILE" +echo "" +echo "7. View Terraform outputs:" +echo " terraform output" + +# Cleanup temporary files +rm -f response.json \ No newline at end of file diff --git a/lambda-managed-instances-tf/variables.tf b/lambda-managed-instances-tf/variables.tf new file mode 100644 index 000000000..950e30b9e --- /dev/null +++ b/lambda-managed-instances-tf/variables.tf @@ -0,0 +1,19 @@ +# Variables for Lambda Managed Instances Terraform implementation + +variable "aws_region" { + description = "AWS region for deployment" + type = string + default = "us-east-1" +} + +variable "environment" { + description = "Environment name" + type = string + default = "demo" +} + +variable "project_name" { + description = "Project name for resource naming" + type = string + default = "lambda-managed-instances" +} \ No newline at end of file