How to create Lambda function with Terraform
Terraform is a great IaaS framework that helps to automate cloud infrastructure easily. We will see how we can create an AWS lambda function with Terraform.
1. Initialize the terraform script with AWS
Create a file called main.tf
in the folder you want to initialize the project. Then add the following provider block to main.tf
which allow it to work with AWS infrastructure as the cloud service provider.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-1"
profile = "test" # name of the profile in credentials file
}
here we have defined profile = "test"
to use of the test
profile from AWS credentials located in ~/.aws/credentials
file.
Next, we have to define the resources to create the lambda function.
2. Create an archive to upload using archive_file
We need to archive the code containing the lambda function. For that, we can use archive_file
data source.
The file structure for the lambda function is as follows.
./lambda
main.tf
/function
index.js
package.json
We need to archive the function folder and upload it to the lambda function. We can achieve it by following code block. This also has another resource that installs node_modules
when running apply before archive using yarn
. The local-exec
command will perform that.
# Archive lambda function
data "archive_file" "main" {
type = "zip"
source_dir = "lambda/function"
output_path = "${path.module}/.terraform/archive_files/function.zip"
depends_on = [null_resource.main]
}
# Provisioner to install dependencies in lambda package before upload it.
resource "null_resource" "main" {
triggers = {
updated_at = timestamp()
}
provisioner "local-exec" {
command = <<EOF
yarn
EOF
working_dir = "${path.module}/lambda/function"
}
}
3. Lambda function
For this example, I have used a simple hello world with a nanoID token.
import {nanoid} from 'nanoid'
export const handler = async (event) => {
console.log('Event: ', JSON.stringify(event));
return {
status: 200,
body: `Hello world! Your token is: ${nanoid()}`
}
}
And the node modules are installed with the following package.json
file.
{
"name": "lambda",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"author": "",
"dependencies": {
"nanoid": "^4.0.0"
}
}
This has nanoid
dependency and it will be installed with the null_resource.main
block using yarn
.
Now we need to create resources to upload the lambda function in AWS account. For that, we can use aws_lambda_function
resource.
resource "aws_lambda_function" "lambda_hello_world" {
filename = "${path.module}/.terraform/archive_files/function.zip"
function_name = "lambda-hello-world"
role = aws_iam_role.lambda_hello_world_role.arn
handler = "index.handler"
runtime = "nodejs16.x"
timeout = 300
source_code_hash = data.archive_file.main.output_base64sha256
}
resource "aws_iam_role" "lambda_hello_world_role" {
name = "lambda_hello_world_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
inline_policy {
name = "lamda-hello-world-policy"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "LambdaHelloWorld1",
"Effect" : "Allow",
"Action" : [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Resource" : "*"
}
]
})
}
}
And also as in the above code we need to define the IAM policy for the lambda function and its defined using aws_iam_role
resource.
The full code will look like the following.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
profile = "profile_name"
}
# Archive lambda function
data "archive_file" "main" {
type = "zip"
source_dir = "lambda/function"
output_path = "${path.module}/.terraform/archive_files/function.zip"
depends_on = [null_resource.main]
}
# Provisioner to install dependencies in lambda package before upload it.
resource "null_resource" "main" {
triggers = {
updated_at = timestamp()
}
provisioner "local-exec" {
command = <<EOF
yarn
EOF
working_dir = "${path.module}/lambda/function"
}
}
resource "aws_lambda_function" "lambda_hello_world" {
filename = "${path.module}/.terraform/archive_files/function.zip"
function_name = "lambda-hello-world"
role = aws_iam_role.lambda_hello_world_role.arn
handler = "index.handler"
runtime = "nodejs16.x"
timeout = 300
# upload the function if the code hash is changed
source_code_hash = data.archive_file.main.output_base64sha256
}
resource "aws_iam_role" "lambda_hello_world_role" {
name = "lambda_hello_world_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
inline_policy {
name = "lamda-hello-world-policy"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "LambdaHelloWorld1",
"Effect" : "Allow",
"Action" : [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Resource" : "*"
}
]
})
}
}
4. How to create the infrastructure
First, download the necessary libraries to run the main.tf file you have created by executing the following
terraform init
After that, you can run terraform apply
to apply the changes to the backend. Note that it will ask for the changes that going to be applied in the backend first. You have to confirm it by typing yes
.
Now you can test your lambda function after the code is applied. And it will print as following.
{
"status": 200,
"body": "Hello world! Your token is: _ffz8vIrfyIFUybwu0v14"
}
5. Conclusion
The following points are not addressed by the implementation
- It’s not minifying the codebase and just copies the
node_modules
directly. - Doesn’t support type checkers like typescript
This can be overcome by using bundlers like webpack. We can dive into this in another post in future on create Lambda function with Terraform and typescript with package bundlers.