Running Infrastructure as Code (IaC) for AWS Lambda Functions on Terraform Cloud

Posted February 29, 2024 by Trevor Roberts Jr ‐ 8 min read

While reviewing my blog infrastructure, I realized I was still using the deprecated go Lambda runtime to invalidate my CloudFront cache. When I went to update my Lambda deployment automation, I realized I deployed it manually 😅. Read on to see how I went about automating the final manual component of my blog infrastructure...

Introduction

A while back, I wrote a blog post about the Golang Lambda function that I use to invalidate my CloudFront cache when I publish a blog article. Unfortunately, the Lambda runtime I originally used (go1.x) is deprecated as of December 2023, and I need to update to a more recent runtime like the provided.al2023 runtime that AWS recommends for compiled languages like C++, Rust, and Go. One benefit to consider when using the OS-only runtime is that it supports Graviton processors for improved costs.

When I read my automation code, I realized that I manually deployed my Lambda function. I decided to remedy that. Normally, I use CDK and Pulumi to automate cloud deployments for my blog articles. For this project, I decided to try out Terraform Cloud. My Terraform module is stored in GitHub, and Terraform Cloud automatically applies my module changes in response to GitHub commits. Terraform Cloud has a generous free tier: up to 500 resources, which should be more than enough for personal projects as well as for start-ups and SMBs to get started quickly.

Getting Started: Security

As I discussed in a previous blog article, I do not believe in using static credentials for application integrations. The first thing I researched with Terraform Cloud was how to use dynamic credentials for it to administer AWS resources. Fortunately, Terraform works with Amazon IAM's support for OIDC providers. The Terraform documentation was fairly thorough on how to get started.

As part of the configuration, I created an IAM role for Terraform Cloud with the minimum required permissions, namely:

  • Amazon IAM permissions to create and manage a role for my Lambda function.
  • AWS Lambda permissions to create and manage my Lambda function.
  • Amazon S3 read access for the bucket containing my compiled Go binary.

In my Terraform Cloud workspace settings, I configured two environment variables to enable the use of the AWS dynamic credentials:

  • TFC_AWS_PROVIDER_AUTH set to true
  • TFC_AWS_RUN_ROLE_ARN set to my IAM role ARN (ex: arn:aws:iam::9028675309:role/terraform_cloud_manage_lambda_functions)

Terraform Module Source Code

Next, I wrote my Terraform module. I'll highlight a few of the interesting sections and include the full source below:

# Create a Lambda function using the OS-only runtime and my compiled source.
resource "aws_lambda_function" "go_lambda" {
  function_name = "go_CloudFront_invalidate"
  handler       = "bootstrap"
  role          = aws_iam_role.go_lambda_role.arn
  architectures = ["arm64"]
  runtime       = "provided.al2023"
  s3_bucket     = "blog-terraform-input-artifacts"
  s3_key        = "goInvalidateCacheNoRPC.zip"
}

If you are not familiar with Terraform, every cloud resource you create is appropriately referred to as a resource in your code. First, I specify the resource type (aws_lambda_function).

function_name and role parameters are required and self-explanatory. Interesting optional parameters include architectures where I specify to use Graviton processors and runtime where I specify the OS-only Lambda runtime I will use for my function (as of the publication date of this article, there are two options provided.al2023 and provided.al2). Finally, I specify the s3_bucket and s3_key for Terraform to find my compiled code.


NOTE

When using the OS-only runtimes, your compiled binary must be called bootstrap. Alternatively, you can create a shell script called bootstrap that is executable and calls your compiled binary.


Here is the module in its entirety:

provider "aws" {
  region = var.region
}

# Create a Lambda function using the OS-only runtime and my compiled source.
resource "aws_lambda_function" "go_lambda" {
  function_name = "go_CloudFront_invalidate"
  handler       = "bootstrap"
  role          = aws_iam_role.go_lambda_role.arn
  architectures = ["arm64"]
  runtime       = "provided.al2023"
  s3_bucket     = "blog-terraform-input-artifacts"
  s3_key        = "goInvalidateCacheNoRPC.zip"
}

# Create an IAM role for the Lambda function to use.
resource "aws_iam_role" "go_lambda_role" {
  name = "go_lambda_role"

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

# Attach policies to the IAM role to grant necessary permissions for the Lambda function.
resource "aws_iam_policy_attachment" "lambda_basic_execution_cloudfront_codepipeline" {
  for_each = toset([
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 
    "arn:aws:iam::aws:policy/CloudFrontFullAccess",
    "arn:aws:iam::aws:policy/AWSCodePipeline_FullAccess"
  ])
  name = "lambda_basic_execution_cloudfront_codepipeline"
  roles      = [aws_iam_role.go_lambda_role.name]
  policy_arn = each.value
}

Terraform Cloud Configuration

Finally, let's configure Terraform Cloud to run our module in our AWS environment!


NOTE

If you already have a Terraform Cloud account, you scan skip to Step 6


Step 1

Create a new Terraform Cloud account, and login. After confirming your e-mail address, you will be presented with the welcome screen. Click on Create a new organization:

Figure 01: Create your Terraform Cloud Organization
Figure 01: Create your Terraform Cloud Organization

Step 2

Specify a name for your new organization to contain all of your projects and workspaces

Figure 02: Confirm your Organization name
Figure 02: Confirm your Organization name

Step 3

Select which workflow you want to use. I selected the Version Control Workflow.

Figure 03: Choose your workflow
Figure 03: Choose your workflow

Step 4

Select which version control provider you want to integrate with. I selected GitHub.

Figure 04: Choose your version control provider
Figure 04: Choose your version control provider

Step 5

After logging in to your desired provider, select the repository containing your Terraform module.

Figure 05: Choose your repo
Figure 05: Choose your repo

Step 6

Create your Terraform Cloud Workspace that will contain all of the resources your repo will create.

Figure 06: Create a Terraform Cloud Workspace
Figure 06: Create a Terraform Cloud Workspace

Step 7

Confirm your Workspace creation succeeded.

Figure 07: Workspace creation success!
Figure 07: Workspace creation success!

Step 8

Review your Workspace Overview page. Notice your repo listed on the right side, and your repo's README.md is available for you to view here. Before we run our module, we need to specify the environment variables I mentioned earlier to get dynamic credentials for our AWS environment. Click on Configure variables.

Figure 08: Terraform Workspace Overview
Figure 08: Terraform Workspace Overview

Step 9

Click the + Add variable button to add your variable.

Figure 09: Add your variable
Figure 09: Add your variable

Step 10

Select Environment variable then specify your desired key and value.

Figure 10: Confirm your environment variable settings
Figure 10: Confirm your environment variable settings

Step 11

Verify that you correctly create both environment variables that are required by the Terraform Cloud OIDC configuration (listed earlier in this article).

Figure 11: Verify your environment variables
Figure 11: Verify your environment variables

Step 12

Now, you're done! You can either manually trigger a run (using the + New run button) or wait until the next commit to your Terraform module repo to apply it to your cloud environment.

Figure 12: ...And now, we wait for the next execution
Figure 12: ...And now, we wait for the next execution

Wrapping Things Up...

In this blog post, I shared how to use Terraform Cloud, a hosted Terraform service, to run your Terraform module. The example I shared involved deploying a Lambda function written in Go that is compiled for Graviton processors. Finally, we discussed Lambda deployment considerations for functions written in Go going forward using the OS-only runtime. If you want to see the Terraform module code, you can find it on GitHub.

If you found this article useful, let me know on LinkedIn!