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:
Step 2
Specify a name for your new organization to contain all of your projects and workspaces
Step 3
Select which workflow you want to use. I selected the Version Control Workflow.
Step 4
Select which version control provider you want to integrate with. I selected GitHub.
Step 5
After logging in to your desired provider, select the repository containing your Terraform module.
Step 6
Create your Terraform Cloud Workspace that will contain all of the resources your repo will create.
Step 7
Confirm your Workspace creation succeeded.
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.
Step 9
Click the + Add variable button to add your variable.
Step 10
Select Environment variable then specify your desired key and value.
Step 11
Verify that you correctly create both environment variables that are required by the Terraform Cloud OIDC configuration (listed earlier in this article).
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.
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!