Solving the Cold Start Challenge for Lambda Function Using Terraform Cloud
Tags: aws, lambd, iac, terraform
Published: 2022-06-10 09:07:16


In this post we are going to properly address the cold starts for Lambda Functions. In a later post we will create a private module which facilitates consumption of this module.

Start in an empty directory and create the following files:

        "main.tf"
        "variables.tf"
        "output.tf"
        "terraform.auto.tfvars"


In the "main.tf" file add the following to it.


        provider "aws" {
            region = "eu-central-1"
        }


One quick note: please NEVER add your credentials here! Ideally, store credentials as environment variables in Terraform Cloud. 
Now open the terminal and execute the "terraform init" command. This will initialize our directory with the AWS provider.
To deploy the lambda function, we will need to upload the packaged code to S3. So I am also leveraging the random provider to make sure that the S3 Bucket is unique.


        resource "random_pet" "lambda_bucket_name" {
          prefix = "test"
          length = 4
        }
        
        
        resource "aws_s3_bucket" "lambda_bucket" {
          bucket = random_pet.lambda_bucket_name.id
          acl    = "private"
            tags = {
            "env" = "test"
          }
        }


The two resources above will just create a random bucket.
The following command will allow me to package the code before uploading it to S3 (I prefer to avoid the local executioner whenever possible):


        data "archive_file" "lambdaFunc_lambda_bucket" {
          type = "zip"
        
          source_dir  = var.src_path
          output_path = var.target_path
        }
        


After that I create the bucket for my deployment artifact:

        resource "aws_s3_bucket_object" "lambdaFunc_lambda_bucket" {
          bucket = aws_s3_bucket.lambda_bucket.id
        
          key    = var.target_path
          source = data.archive_file.lambdaFunc_lambda_bucket.output_path
        
          etag = filemd5(data.archive_file.lambdaFunc_lambda_bucket.output_path)
            tags = {
            "env" = "test"
          }
        }



Now things get more interesting. When you deploy a lambda function, you can specify a few parameters that are relevant when you want to deploy in production. One is of course, the reserved concurrency. AWS limits the number of concurrent executions per account and per region. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ysa34t25ia444488o2rj.png)

Therefore for having predictability for our concurrency and not having our Lambda function throttled by other lambdas, we set this parameter.



        resource "aws_lambda_function" "lambdaFunc" {
          function_name = var.function_name
        
          s3_bucket = aws_s3_bucket.lambda_bucket.id
          s3_key    = aws_s3_bucket_object.lambdaFunc_lambda_bucket.key
        
          runtime = var.lambda_runtime
          handler = var.handler
        
          source_code_hash = data.archive_file.lambdaFunc_lambda_bucket.output_base64sha256
        
          role                           = aws_iam_role.lambda_exec.arn
          reserved_concurrent_executions = var.concurrent_executions
          tags = {
            "env" = "test"
          }
        }    



Now we get to the most important port, Cold Start. As officially suggested by AWS, I will be leveraging provisioned concurrency to deploy the lambda function. There are many solutions which use aws cloud watch as rules, but I really believe that provisioned concurrency is much simpler and comfortable to use. Moreover, in order to use the provisioned concurrency, we will need to create an Alias. This Alias can also serve as a way to do Blue Green or Canary deployments, which we will hopefully cover in a future post.



        resource "aws_lambda_alias" "con_lambda_alias" {
          name             = "lambda_alias"
          description      = "for blue green deployments OR for concurrency"
          function_name    = aws_lambda_function.lambdaFunc.arn
          function_version = var.function_version
        }
        
        resource "aws_lambda_provisioned_concurrency_config" "config" {
          function_name                     = aws_lambda_alias.con_lambda_alias.function_name
          provisioned_concurrent_executions = var.provisioned_concurrent_executions
          qualifier                         = aws_lambda_alias.con_lambda_alias.name
        }


Please note that I used variables as the value for these parameters. These variables have defaults. Feel free to change them as your use case requires. Also please be aware that the provisioned concurrency is going to cost you as AWS keeps a warm instance of your application somewhere.

Lastly, we need a basic policy for our lambda function.

        resource "aws_iam_role" "lambda_exec" {
          name = "serverless_lambda"
          assume_role_policy = jsonencode(
            {
              "Version" : "2012-10-17",
              "Statement" : [
                {
                  "Effect" : "Allow",
                  "Principal" : {
                    "Service" : "lambda.amazonaws.com"
                  },
                  "Action" : "sts:AssumeRole"
                }
              ]
            }
          )
        }


In the "variables.tf" file add the following to it:

        variable "function_name" {
          type    = string
          default = "test-function"
        }
        
        variable "src_path" {
          type = string
        }
        
        variable "target_path" {
          type = string
        }
        
        variable "lambda_runtime" {
          type    = string
          default = "nodejs12.x"
        }
        
        variable "handler" {
          type    = string
          default = "index.handler"
        }
        
        variable "region" {
          type    = string
          default = "eu-central-1"
        }
        
        variable "concurrent_executions" {
          type    = string
          default = "1"
        }
        
        variable "provisioned_concurrent_executions" {
          type    = string
          default = "1"
        }
        
        variable "function_version" {
          type    = string
          default = "1"
        }


And in the "terraform.auto.tfvars" file add the following content:

        src_path    = "./src"  //the path to the source code for lambdafunction
        target_path = "./artifacts/lambda_deployment.zip" //the path for the deployment artifact


You can of course bring your own lambda function, but if you don't have one, create a folder called "src and a file in it called "index.handler" and add the following to it:

        exports.handler =  async (event) => {
          const payload = {
            date: new Date(),
            message: 'Terraform is awesome!'
          };
          return JSON.stringify(payload);
        };

Now you can deploy the application using Terraform CLI or Terraform Cloud. For the CLI version, simply run
        
        "terraform apply -auto-approve"

If you want to use Terraform Cloud to have "GitOps" use the following documentation:

https://www.hashicorp.com/resources/a-practitioner-s-guide-to-using-hashicorp-terraform-cloud-with-github

This problem might have been difficult to solve, but I can guarantee that it is much harder to communicate the solution to someone properly.
I will create another blog post explaining how you can optimally share this solution with someone inside your organization.

GitHub Repository: https://github.com/pedramha/terraform-aws-lambda
Youtube: https://www.youtube.com/watch?v=e0QplrqH0J4


Thank you!
Pedram