Setting Up VPC and Lambda Function with Terraform

RMAG news

In my previous post, Setting Up a VPC for Your App Using AWS Management Console, I talked about how to set up a VPC using the AWS Management Console.

In this blog, I will show you how to achieve a similar setup using Terraform. I have a Lambda function that is behind a custom domain API Gateway. This Lambda function interacts with DynamoDB (without going through the internet) and some third-party APIs.

Below is the Terraform code to set up the required infrastructure.

Terraform Code

main.tf File

resource “aws_vpc” “main” {
cidr_block = “<YOUR_PREFERRED_IP_CIDR_BLOCK>”
}

# Create private subnets
resource “aws_subnet” “private” {
count = <NUMBER_OF_SUBNETS_YOU_WANT>
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index * 2 + 1)
availability_zone = element(data.aws_availability_zones.available.names, count.index)
map_public_ip_on_launch = false
}

# Create public subnets
resource “aws_subnet” “public” {
count = <NUMBER_OF_SUBNETS_YOU_WANT>
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index * 2)
availability_zone = element(data.aws_availability_zones.available.names, count.index)
map_public_ip_on_launch = true
}

# Create Internet Gateway
resource “aws_internet_gateway” “igw” {
vpc_id = aws_vpc.main.id
}

# Create Elastic IP for NAT Gateway
resource “aws_eip” “nat” {
domain = “vpc”
}

# Create NAT Gateway
resource “aws_nat_gateway” “nat” {
allocation_id = aws_eip.nat.id
subnet_id = element(aws_subnet.public.*.id, 0)
}

# Create public route table
resource “aws_route_table” “public” {
vpc_id = aws_vpc.main.id

route {
cidr_block = “0.0.0.0/0”
gateway_id = aws_internet_gateway.igw.id
}
}

# Associate public subnets with public route table
resource “aws_route_table_association” “public” {
count = length(aws_subnet.public.*.id)
subnet_id = element(aws_subnet.public.*.id, count.index)
route_table_id = aws_route_table.public.id
}

# Create private route table
resource “aws_route_table” “private” {
vpc_id = aws_vpc.main.id

route {
cidr_block = “0.0.0.0/0”
nat_gateway_id = aws_nat_gateway.nat.id
}
}

# Associate private subnets with private route table
resource “aws_route_table_association” “private” {
count = length(aws_subnet.private.*.id)
subnet_id = element(aws_subnet.private.*.id, count.index)
route_table_id = aws_route_table.private.id
}

# Create security group for Lambda
resource “aws_security_group” “lambda” {
name = “lambda-sg”
description = “Security group for Lambda”
vpc_id = aws_vpc.main.id

egress {
from_port = 443
to_port = 443
protocol = “TCP”
cidr_blocks = [aws_vpc.main.cidr_block]
}
}

# Lambda VPC Endpoint for DynamoDB
resource “aws_vpc_endpoint” “dynamodb” {
vpc_id = aws_vpc.main.id
service_name = “com.amazonaws.${var.region}.dynamodb”
vpc_endpoint_type = “Gateway”
route_table_ids = aws_route_table.private[*].id
}
# The above terraform command automatically creates prefix list in the private route table.

# Lambda Function
resource “aws_lambda_function” “lambda_function” {
filename = <PATH_TO_YOUR_ZIP_FILE.zip>
function_name = “<LAMBDA_FUNCTION_NAME>”
role = aws_iam_role.lambda_role.arn
handler = “index.handler”
runtime = “nodejs20.x”
memory_size = 512
timeout = 10

logging_config {
log_format = “Text”
log_group = “/aws/lambda/<LAMBDA_FUNCTION_NAME>”
}
tracing_config {
mode = “PassThrough”
}

ephemeral_storage {
size = “512”
}

vpc_config {
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.lambda.id]
}
}

resource “aws_lambda_alias” “lambda_alias” {
name = var.stage
function_name = aws_lambda_function.lambda_function.function_name
function_version = aws_lambda_function.lambda_function.version
}

resource “aws_lambda_permission” “apigw” {
statement_id = “AllowAPIGatewayInvoke”
action = “lambda:InvokeFunction”
function_name = aws_lambda_function.lambda_function.function_name
principal = “apigateway.amazonaws.com”
source_arn = “${aws_api_gateway_rest_api.api.execution_arn}/${var.stage}/*”
}

resource “aws_iam_role” “lambda_role” {
name = “lambdaRole”

assume_role_policy = jsonencode({
Version = “2012-10-17”
Statement = [
{
Action = “sts:AssumeRole”
Effect = “Allow”
Principal = {
Service = “lambda.amazonaws.com”
}
}
]
})
}

# DynamoDB Tables
resource “aws_dynamodb_table” “dynamo_table” {
// YOUR TABLE CONFIG
}

resource “aws_iam_role_policy” “lambda_policy” {
name = “lambda-policy”
role = aws_iam_role.lambda_role.id

policy = jsonencode({
Version = “2012-10-17”,
Statement = [
{
Effect = “Allow”,
Action = [
“logs:CreateLogGroup”,
“logs:CreateLogStream”,
“logs:TagResource”,
“logs:PutLogEvents”
],
Resource = [
“arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/<LAMBDA_FUNCTION_NAME>-${var.stage}*:*”
]
},
{
Effect = “Allow”,
Action = [
“logs:PutLogEvents”
],
Resource = [
“arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/<LAMBDA_FUNCTION_NAME>-${var.stage}*:*:*”
]
},
{
Effect = “Allow”,
Action = [
“dynamodb:Query”,
“dynamodb:Scan”,
“dynamodb:GetItem”,
“dynamodb:PutItem”,
“dynamodb:UpdateItem”,
“dynamodb:DeleteItem”
],
Resource = [
aws_dynamodb_table.dynamo_table.arn
]
},
{
Effect = “Allow”,
Action = [
“ec2:CreateNetworkInterface”,
“ec2:DescribeNetworkInterfaces”,
“ec2:DeleteNetworkInterface”,
“ec2:DescribeInstances”,
“ec2:AttachNetworkInterface”
],
Resource = “*”
}
]
})
}

resource “aws_iam_role_policy_attachment” “policy_attach” {
role = aws_iam_role.lambda_role.name
policy_arn = “arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole”
}

# API Gateway
resource “aws_api_gateway_rest_api” “api” {
name = aws_lambda_function.lambda_function.function_name
description = “API for your Service in ${var.stage} environment”
}

resource “aws_api_gateway_resource” “proxy” {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = “{proxy+}”
}

resource “aws_api_gateway_method” “proxy_method” {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = “ANY”
authorization = “NONE”
}

resource “aws_api_gateway_integration” “proxy_integration” {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = aws_api_gateway_method.proxy_method.http_method
type = “AWS_PROXY”
integration_http_method = “POST”
uri = aws_lambda_function.lambda_function.invoke_arn
}

resource “aws_api_gateway_deployment” “api_deployment” {
depends_on = [
aws_api_gateway_method.proxy_method,
aws_api_gateway_integration.proxy_integration,
]
rest_api_id = aws_api_gateway_rest_api.api.id
lifecycle {
create_before_destroy = true
}
}

resource “aws_api_gateway_stage” “api_stage” {
stage_name = var.stage
rest_api_id = aws

_api_gateway_rest_api.api.id
deployment_id = aws_api_gateway_deployment.api_deployment.id
}

resource “aws_api_gateway_base_path_mapping” “base_path_mapping” {
depends_on = [aws_api_gateway_stage.api_stage]
domain_name = var.api_gateway_domain_name
api_id = aws_api_gateway_rest_api.api.id
stage_name = aws_api_gateway_stage.api_stage.stage_name
base_path = var.base_path
}

data “aws_caller_identity” “current” {}

Notice the use of subnet and security group while creating the Lambda function.

Note: If you are using VPC Endpoint of type Gateway than you dont need to make any changes in your code specially in dynamo db configurations.

Conclusion

By following these steps and utilizing the Terraform code provided, you can set up a secure and efficient environment for your application on AWS. This setup involves creating and configuring a VPC, subnets, route tables, internet gateway, NAT gateway, VPC endpoints, and associating them appropriately. Following these steps will help you establish a secure and efficient environment for your application on AWS.

Please follow and like us:
Pin Share