diff --git a/terraform-lambda-aurora-serverless/.gitignore b/terraform-lambda-aurora-serverless/.gitignore new file mode 100644 index 000000000..9ca79959d --- /dev/null +++ b/terraform-lambda-aurora-serverless/.gitignore @@ -0,0 +1,3 @@ +builds/* +.terraform +.terraform.* diff --git a/terraform-lambda-aurora-serverless/README.md b/terraform-lambda-aurora-serverless/README.md new file mode 100644 index 000000000..daac6d434 --- /dev/null +++ b/terraform-lambda-aurora-serverless/README.md @@ -0,0 +1,49 @@ +# AWS Lambda function to Amazon Aurora Serverless + +The pattern creates a Lambda function and a Amazon Aurora Serverless cluster, a Log group and the IAM resources required to run the application. + +The Lambda function is written in Python that uses pymysql client to establish connectivity with the serverless database. + +## Getting started with Terraform Serverless Patterns + +Read more about general requirements and deployment instructions for Terraform Serverless Patterns [here](https://github.com/aws-samples/serverless-patterns/blob/main/terraform-fixtures/docs/README.md). + +## Steps + +First of all, you will need to install the 'pymysql' client depedency which is used in the Lambda function code. +```shell +cd src/function +pip3 install -r requirements.txt -t . +cd ../.. +``` +Then perform the following terraform commands to deploy the stack +```shell +terraform init +terrform deploy +``` + +## Testing + +After deployment, invoke Lambda function with multiple inputs, and go to the Step Function Console and view the different invocations to note the different behavior with the different inputs. + +To do this, you can run these commands in the terminal (replace `` with the value returned in `lambda_function_name`): + +```shell +aws lambda invoke --function-name --payload '{"key": "value"}' response.json +``` +## Output + +Upon successful invocation, the function returns the following response - + +```json +{ + "statusCode": 200, + "body": "{\"message\": \"Successfully connected to the database\", \"database\": \"mydb\", \"host\": \"aurora-serverless-cluster.cluster-cna4c0mg426r.us-east-1.rds.amazonaws.com\"}" +} +``` + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= "5.84.0" | \ No newline at end of file diff --git a/terraform-lambda-aurora-serverless/example-pattern.json b/terraform-lambda-aurora-serverless/example-pattern.json new file mode 100644 index 000000000..6c3b249c3 --- /dev/null +++ b/terraform-lambda-aurora-serverless/example-pattern.json @@ -0,0 +1,65 @@ +{ + "title": "AWS Lambda function to Amazon Aurora Serverless", + "description": "The pattern creates a Lambda function and an Amazon Aurora Cluster with Serverless instance.", + "language": "Python", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "The pattern creates a Lambda function and an Amazon Aurora Cluster with Serverless instance.", + "It also guides how to invoke the function to access the database." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/terraform-lambda-aurora-serverless", + "templateURL": "serverless-patterns/terraform-lambda-aurora-serverless", + "projectFolder": "terraform-lambda-aurora-serverless", + "templateFile": "terraform-lambda-aurora-serverless/main.tf" + } + }, + "resources": { + "bullets": [ + { + "test": "Terraform AWS Lambda examples", + "link": "https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples" + }, + { + "test": "Terraform Registry - RDS (Relational Database)", + "link": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster.html" + }, + { + "text": "Amazon Aurora User Guide", + "link": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html" + }, + { + "test": "AWS Lambda Developer Guide", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + } + ] + }, + "deploy": { + "text": [ + "terraform init && terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + ] + }, + "authors": [ + { + "name": "Saborni Bhattacharya", + "image": "https://drive.google.com/file/d/1AZFquOkafEQRUlrT4hKOtIbt4Cq66SHd/view?usp=sharing", + "bio": "AWS SA, Cloud Enthusiast", + "linkedin": "https://www.linkedin.com/in/saborni-bhattacharya-5b523812a/" + } + ] +} diff --git a/terraform-lambda-aurora-serverless/main.tf b/terraform-lambda-aurora-serverless/main.tf new file mode 100644 index 000000000..5d082519a --- /dev/null +++ b/terraform-lambda-aurora-serverless/main.tf @@ -0,0 +1,284 @@ +# VPC and Networking +resource "aws_vpc" "lambda_vpc" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "lambda-aurora-vpc" + Environment = var.environment + } +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.lambda_vpc.id + + tags = { + Name = "main-igw" + Environment = var.environment + } +} + +# Public Subnet for NAT Gateway +resource "aws_subnet" "public" { + vpc_id = aws_vpc.lambda_vpc.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, 0) + availability_zone = "${var.aws_region}a" + map_public_ip_on_launch = true + + tags = { + Name = "public-subnet" + Environment = var.environment + } +} + +# Private Subnets for Lambda and Aurora +resource "aws_subnet" "lambda_subnet_1" { + vpc_id = aws_vpc.lambda_vpc.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, 1) + availability_zone = "${var.aws_region}a" + + tags = { + Name = "lambda-aurora-subnet-1" + Environment = var.environment + } +} + +resource "aws_subnet" "lambda_subnet_2" { + vpc_id = aws_vpc.lambda_vpc.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, 2) + availability_zone = "${var.aws_region}b" + + tags = { + Name = "lambda-aurora-subnet-2" + Environment = var.environment + } +} + +# NAT Gateway +resource "aws_eip" "nat" { + domain = "vpc" +} + +resource "aws_nat_gateway" "main" { + allocation_id = aws_eip.nat.id + subnet_id = aws_subnet.public.id + + tags = { + Name = "main-nat" + Environment = var.environment + } +} + +# Route Tables +resource "aws_route_table" "public" { + vpc_id = aws_vpc.lambda_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = { + Name = "public-rt" + Environment = var.environment + } +} + +resource "aws_route_table" "private" { + vpc_id = aws_vpc.lambda_vpc.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main.id + } + + tags = { + Name = "private-rt" + Environment = var.environment + } +} + +# Route Table Associations +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "private_1" { + subnet_id = aws_subnet.lambda_subnet_1.id + route_table_id = aws_route_table.private.id +} + +resource "aws_route_table_association" "private_2" { + subnet_id = aws_subnet.lambda_subnet_2.id + route_table_id = aws_route_table.private.id +} + +# Security Groups +resource "aws_security_group" "aurora_sg" { + name = "aurora-security-group" + description = "Security group for Aurora Serverless" + vpc_id = aws_vpc.lambda_vpc.id + + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "aurora-sg" + Environment = var.environment + } +} + +# Aurora Configuration +resource "aws_db_subnet_group" "aurora_subnet_group" { + name = "aurora-subnet-group" + subnet_ids = [aws_subnet.lambda_subnet_1.id, aws_subnet.lambda_subnet_2.id] + + tags = { + Name = "Aurora subnet group" + Environment = var.environment + } +} + +resource "aws_rds_cluster" "aurora_cluster" { + cluster_identifier = "aurora-serverless-cluster" + engine = "aurora-mysql" + engine_version = "8.0.mysql_aurora.3.04.1" + engine_mode = "provisioned" + database_name = var.database_name + master_username = var.db_username + master_password = var.db_password + storage_encrypted = true + skip_final_snapshot = true + db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name + vpc_security_group_ids = [aws_security_group.aurora_sg.id] + + serverlessv2_scaling_configuration { + min_capacity = 0.5 + max_capacity = 16.0 + } + + tags = { + Environment = var.environment + } + # Add explicit dependencies + depends_on = [ + aws_vpc.lambda_vpc, + aws_subnet.lambda_subnet_1, + aws_subnet.lambda_subnet_2, + aws_security_group.aurora_sg + ] +} + +# Create Aurora Instance +resource "aws_rds_cluster_instance" "aurora_instance" { + cluster_identifier = aws_rds_cluster.aurora_cluster.id + instance_class = "db.serverless" + engine = aws_rds_cluster.aurora_cluster.engine + engine_version = aws_rds_cluster.aurora_cluster.engine_version + + depends_on = [aws_rds_cluster.aurora_cluster] +} + +# IAM Configuration +resource "aws_iam_role" "lambda_role" { + name = "lambda_aurora_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "lambda_aurora_policy" + role = aws_iam_role.lambda_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "rds-data:ExecuteStatement", + "rds-data:BatchExecuteStatement", + "rds-data:BeginTransaction", + "rds-data:CommitTransaction", + "rds-data:RollbackTransaction", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface" + ] + Resource = "*" + } + ] + }) +} + +# Lambda Function +data "archive_file" "lambda_zip" { + type = "zip" + source_dir = "${path.module}/src/function" + output_path = "${path.module}/function.zip" +} + +resource "aws_lambda_function" "aurora_lambda" { + depends_on = [ + aws_vpc.lambda_vpc, + aws_subnet.lambda_subnet_1, + aws_subnet.lambda_subnet_2, + aws_security_group.aurora_sg, + aws_rds_cluster.aurora_cluster, + aws_iam_role.lambda_role + ] + + function_name = var.lambda_function_name + role = aws_iam_role.lambda_role.arn + filename = data.archive_file.lambda_zip.output_path + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + handler = var.lambda_handler + runtime = var.lambda_runtime + timeout = var.lambda_timeout + memory_size = var.lambda_memory_size + + environment { + variables = { + DB_ENDPOINT = aws_rds_cluster.aurora_cluster.endpoint + DB_NAME = var.database_name + DB_USERNAME = var.db_username + DB_PASSWORD = var.db_password + } + } + + vpc_config { + subnet_ids = [aws_subnet.lambda_subnet_1.id, aws_subnet.lambda_subnet_2.id] + security_group_ids = [aws_security_group.aurora_sg.id] + } + + tags = { + Environment = var.environment + } +} diff --git a/terraform-lambda-aurora-serverless/outputs.tf b/terraform-lambda-aurora-serverless/outputs.tf new file mode 100644 index 000000000..19972e116 --- /dev/null +++ b/terraform-lambda-aurora-serverless/outputs.tf @@ -0,0 +1,29 @@ +output "lambda_function_arn" { + description = "ARN of the Lambda function" + value = aws_lambda_function.aurora_lambda.arn +} + +output "lambda_function_name" { + description = "Name of the Lambda function" + value = aws_lambda_function.aurora_lambda.function_name +} + +output "aurora_cluster_endpoint" { + description = "Aurora cluster endpoint" + value = aws_rds_cluster.aurora_cluster.endpoint +} + +output "aurora_cluster_arn" { + description = "Aurora cluster ARN" + value = aws_rds_cluster.aurora_cluster.arn +} + +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.lambda_vpc.id +} + +output "subnet_ids" { + description = "IDs of the subnets" + value = [aws_subnet.lambda_subnet_1.id, aws_subnet.lambda_subnet_2.id] +} \ No newline at end of file diff --git a/terraform-lambda-aurora-serverless/src/function/app.py b/terraform-lambda-aurora-serverless/src/function/app.py new file mode 100755 index 000000000..ea0268fdb --- /dev/null +++ b/terraform-lambda-aurora-serverless/src/function/app.py @@ -0,0 +1,53 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import os +import json +import pymysql +import logging + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def lambda_handler(event, context): + # Get database connection info from environment variables + host = os.environ['DB_ENDPOINT'] + database = os.environ['DB_NAME'] + username = os.environ['DB_USERNAME'] + password = os.environ['DB_PASSWORD'] + + try: + # Attempt database connection + conn = pymysql.connect( + host=host, + user=username, + password=password, + database=database, + connect_timeout=5 + ) + + # Execute simple test query + with conn.cursor() as cursor: + cursor.execute("SELECT 1") + result = cursor.fetchone() + + conn.close() + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Successfully connected to the database', + 'database': database, + 'host': host + }) + } + + except Exception as e: + logger.error(f"Database connection failed: {str(e)}") + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to connect to database', + 'error': str(e) + }) + } diff --git a/terraform-lambda-aurora-serverless/src/function/requirements.txt b/terraform-lambda-aurora-serverless/src/function/requirements.txt new file mode 100644 index 000000000..d3179a585 --- /dev/null +++ b/terraform-lambda-aurora-serverless/src/function/requirements.txt @@ -0,0 +1 @@ +pymysql==1.1.0 \ No newline at end of file diff --git a/terraform-lambda-aurora-serverless/variables.tf b/terraform-lambda-aurora-serverless/variables.tf new file mode 100644 index 000000000..cbdfc49dc --- /dev/null +++ b/terraform-lambda-aurora-serverless/variables.tf @@ -0,0 +1,83 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "environment" { + description = "Environment name" + type = string + default = "dev" +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.100.0.0/16" +} + +variable "db_username" { + description = "Database administrator username" + type = string + default = "admin" +} + +variable "db_password" { + description = "Database administrator password, please change it" + type = string + sensitive = true +} + +variable "database_name" { + description = "Name of the database" + type = string + default = "mydb" +} + +variable "aurora_min_capacity" { + description = "Minimum Aurora capacity unit" + type = number + default = 1 +} + +variable "aurora_max_capacity" { + description = "Maximum Aurora capacity unit" + type = number + default = 2 +} + +variable "lambda_function_name" { + description = "Name of the Lambda function" + type = string + default = "aurora-lambda" +} + +variable "lambda_src_path" { + description = "Path to the Lambda function zip file" + type = string + default = "src/function" +} + +variable "lambda_handler" { + description = "Lambda function handler" + type = string + default = "app.lambda_handler" +} + +variable "lambda_runtime" { + description = "Lambda function runtime" + type = string + default = "python3.12" +} + +variable "lambda_timeout" { + description = "Lambda function timeout in seconds" + type = number + default = 30 +} + +variable "lambda_memory_size" { + description = "Lambda function memory size in MB" + type = number + default = 128 +} diff --git a/terraform-lambda-aurora-serverless/versions.tf b/terraform-lambda-aurora-serverless/versions.tf new file mode 100644 index 000000000..852c2dad1 --- /dev/null +++ b/terraform-lambda-aurora-serverless/versions.tf @@ -0,0 +1,14 @@ + + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.84.0" + } + } +} + +provider "aws" { + region = var.aws_region +} \ No newline at end of file