Small lambda function with Python and Terraform

Ats
4 min readApr 27, 2024

--

I made a small lambda function without SAM. This is the document of it.

Photo by Daniel K Cheung on Unsplash

Background

I usually use AWS SAM to create a serverless application because it provides everything like infrastructure definition, application code, deployment, and local development environment with docker. However, I felt it would be overspec because I wanted to create a small function which has 30-lines python code and just bridges a request to another endpoint. I tested the small code line in my jupter notebook and looked for the place to execute the code snippets. Then I challenged to create Lambda env with Terraform.

What I did

I checked the official docs by Terraform first. The docs is quite old. So I looked for other articles.

Then I googled the way to do with Terraform and Lambda and I decided to use the article as a reference.

The reason why I chose it was because I didn’t create a docker image. I knew I could the same thing with docker but I didn’t want to use it because my code was just 30-lines and docker was too much for the purpose. However, if you don’t use docker for this purpose, you can’t use pip package. In my case, I could manage what I wanted to do with packages provided by Python itself. But it could be a blocker for the way to do without docker image.

This is about the way to do with Python. This is pretty much about what I did.

Actually, I did almost same as the reference article and mixed the python way into it. But I changed the settings for CORS. In the reference, the CORS check uses OPTION method before sending GET or POST method request. So I explicitly created another aws_api_gateway_method_response for the OPTION method. I got the code snippets from the following article.

Then the my final output looks like below. I skipped the code lines for cognito and route53 because they are same as the reference.

  • For lambda
resource "aws_lambda_function" "my_lambda_name" {
function_name = "my_lambda_name_function_name"

filename = "lambda_function.zip"
source_code_hash = data.archive_file.python_lambda_package.output_base64sha256

handler = "lambda_function.lambda_handler"
runtime = "python3.11"

role = aws_iam_role.my_exec_iam.arn
}

data "archive_file" "python_lambda_package" {
type = "zip"
source_file = "${path.module}/lambda/lambda_function.py"
output_path = "lambda_function.zip"
}

resource "aws_iam_role" "my_exec_iam" {
name = "my_exec_iam_name"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"

principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}

actions = ["sts:AssumeRole"]
}
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.my_exec_iam.name
}

resource "aws_lambda_permission" "apigw_lambda" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.my_lambda_name.function_name
principal = "apigateway.amazonaws.com"

source_arn = "${aws_api_gateway_rest_api.my_lambda_name.execution_arn}/*/*/*"
}
  • For API Gateway
resource "aws_api_gateway_rest_api" "my_gateway" {
name = "my_gateway_name"
description = "My gateway description"
}

resource "aws_api_gateway_resource" "my_gateway" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
parent_id = aws_api_gateway_rest_api.my_gateway.root_resource_id
path_part = "my_gateway"
}

resource "aws_api_gateway_method" "options_method_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = "OPTIONS"
authorization = "NONE"
}

resource "aws_api_gateway_method" "get_method_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = "GET"
authorization = "NONE"

request_parameters = {
"method.request.querystring.id" = true
"method.request.querystring.key" = true
}
}

resource "aws_api_gateway_integration" "lambda_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = aws_api_gateway_method.get_method_prod.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.my_gateway.invoke_arn
}

resource "aws_api_gateway_method_response" "response_200_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = aws_api_gateway_method.get_method_prod.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
}

resource "aws_api_gateway_method_response" "options_200_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = aws_api_gateway_method.options_method_prod.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true,
"method.response.header.Access-Control-Allow-Methods" = true,
"method.response.header.Access-Control-Allow-Origin" = true
}
}

resource "aws_api_gateway_integration" "options_integration_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = aws_api_gateway_method.options_method_prod.http_method
type = "MOCK"
request_templates = {
"application/json" = jsonencode({ statusCode = 200 })
}
}

resource "aws_api_gateway_integration_response" "options_integration_response_prod" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
resource_id = aws_api_gateway_resource.my_gateway.id
http_method = aws_api_gateway_method.options_method_prod.http_method
status_code = aws_api_gateway_method_response.options_200_prod.status_code
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
"method.response.header.Access-Control-Allow-Methods" = "'GET,OPTIONS'",
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
}

resource "aws_api_gateway_deployment" "my_gateway" {
rest_api_id = aws_api_gateway_rest_api.my_gateway.id
stage_name = "prod"

triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.my_gateway.id,
aws_api_gateway_method.get_method_prod.id,
aws_api_gateway_method.options_method_prod.id,
aws_api_gateway_method_response.response_200_prod.id,
aws_api_gateway_method_response.options_200_prod.id,
aws_api_gateway_integration.lambda_prod.id,
aws_api_gateway_integration.options_integration_prod.id,
aws_api_gateway_integration_response.options_integration_response_prod.id,
]))
}

lifecycle {
create_before_destroy = true
}
}

After all, I felt SAM was convenient because it would set up all of them including API Gateway. I would say SAM is suitable even if the code lines are quite small.

That’s it!

--

--

Ats
Ats

Written by Ats

I like building something tangible like touch, gesture, and voice. Ruby on Rails / React Native / Yocto / Raspberry Pi / Interaction Design / CIID IDP alumni

No responses yet