How To Create AWS Lambda Functions Using Pulumi And Golang
Leon Stigter
Posted on January 29, 2020
I’ve looked at Pulumi to do a bunch of things, including creating subnets in a VPC, building EKS clusters, and DynamoDB tables. The one thing I hadn’t explored yet was how to deploy AWS Lambda functions using Pulumi, so that’s exactly what this blog is about.
The complete project is available on GitHub.
My Lambda
The Lambda function for this post is a rather simple one, it says “Hello” to whatever the value of an environment variable will be.
package main
import (
"fmt"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
val := os.Getenv("NAME")
return events.APIGatewayProxyResponse{
Body: fmt.Sprintf("Hello, %s", val),
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
That code is in a file called hello-world.go , which is in a folder called hello-world. The Pulumi project is inside the folder called Pulumi so it will be a separate folder structure and not collide with the other code of your Lambda functions. The folder structure, including all the files looks like
├── README.md
├── go.mod
├── go.sum
└── hello-world
│ └── main.go
├── pulumi
│ ├── Pulumi.lambdastack.yaml
│ ├── Pulumi.yaml
└── main.go
Building and uploading your Lambda code
In order to deploy a Lambda function, the code needs to be uploaded as well. While Pulumi has the concept of an Archive to create the zip file, the Go implementation has a known issue which makes it impossible to use that feature. Rather than manually building, zipping, and uploading the code, you can extend the Pulumi program a little to do all of that before the program runs.
const (
shell = "sh"
shellFlag = "-c"
rootFolder = "/rootfolder/of/your/lambdaapp"
)
func runCmd(args string) error {
cmd := exec.Command(shell, shellFlag, args)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = rootFolder
return cmd.Run()
}
The runCmd
method will run a specific command and will return either an error or a nil object. To build, zip, and upload your Go code to S3 you can add the below snippets to your Pulumi project. It uses the above function to run the three commands you would normally script as part of your CI/CD or test framework. These commands should be pasted before pulumi.Run()
gets invoked.
if err := runCmd("GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world"); err != nil {
fmt.Printf("Error building code: %s", err.Error())
os.Exit(1)
}
if err := runCmd("zip -r -j ./hello-world/hello-world.zip ./hello-world/hello-world"); err != nil {
fmt.Printf("Error creating zipfile: %s", err.Error())
os.Exit(1)
}
if err := runCmd("aws s3 cp ./hello-world/hello-world.zip s3://<your-bucket>/hello-world.zip"); err != nil {
fmt.Printf("Error creating zipfile: %s", err.Error())
os.Exit(1)
}
If these commands fail, you’ll be able to see the output and the error message as part of the “diagnostics” section in your terminal.
Creating an IAM role
Every Lambda function, and most other AWS resources as well, need an IAM role to be able to work. The IAM role gives the resources the permissions to act on your behalf. This Lambda function doesn’t have a lot of specifics it needs to do, other than be able to run. To create an IAM role with that permission, you can use the below code. The ARN (Amazon Resource Name), is exported so that it is visible from within the Pulumi console.
// The policy description of the IAM role, in this case only the sts:AssumeRole is needed
roleArgs := &iam.RoleArgs{
AssumeRolePolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}`,
}
// Create a new role called HelloWorldIAMRole
role, err := iam.NewRole(ctx, "HelloWorldIAMRole", roleArgs)
if err != nil {
fmt.Printf("role error: %s\n", err.Error())
return err
}
// Export the role ARN as an output of the Pulumi stack
ctx.Export("Role ARN", role.Arn())
Setting environment variables
Just like in CloudFormation, the Pulumi SDK allows you to create a set of environment variables. That’s a good thing, because the Lambda function relies on an environment variable called “NAME” to know who to greet. The environment variables are a map[string]interface{}
inside a map[string]interface{}
. For this Lambda function, that would be the below snippet.
environment := make(map[string]interface{})
variables := make(map[string]interface{})
variables["NAME"] = "WORLD"
environment["variables"] = variables
Creating the function
The last step. at least for this code, is to actually create the Lambda function. The S3Bucket and S3Key are the name of the bucket and the name of the file you’ve uploaded earlier in the process. The role.Arn() is the ARN of the role that was created in the previous step.
// The set of arguments for constructing a Function resource.
functionArgs := &lambda.FunctionArgs{
Description: "My Lambda function",
Runtime: "go1.x",
Name: "HelloWorldFunction",
MemorySize: 256,
Timeout: 10,
Handler: "hello-world",
Environment: environment,
S3Bucket: "<your-bucket>",
S3Key: "hello-world.zip",
Role: role.Arn(),
}
// NewFunction registers a new resource with the given unique name, arguments, and options.
function, err := lambda.NewFunction(ctx, "HelloWorldFunction", functionArgs)
if err != nil {
fmt.Println(err.Error())
return err
}
// Export the function ARN as an output of the Pulumi stack
ctx.Export("Function", function.Arn())
Running Pulumi up
With all the code ready, it’s time to run pulumi up
and deploy the Lambda function to AWS. If you need more details on how to create a Go project for Pulumi, check out this post.
$ pulumi up
Previewing update (lambda):
Type Name Plan Info
+ pulumi:pulumi:Stack lambda-lambda create 2 messages
+ ├─ aws:iam:Role HelloWorldIAMRole create
+ └─ aws:lambda:Function HelloWorldFunction create
Diagnostics:
pulumi:pulumi:Stack (lambda-lambda):
updating: hello-world/hello-world (deflated 49%)
upload: hello-world/hello-world.zip to s3://<your-bucket>/hello-world.zip
Resources:
+ 3 to create
Do you want to perform this update? yes
Updating (lambda):
Type Name Status Info
+ pulumi:pulumi:Stack lambda-lambda created 2 messages
+ ├─ aws:iam:Role HelloWorldIAMRole created
+ └─ aws:lambda:Function HelloWorldFunction created
Diagnostics:
pulumi:pulumi:Stack (lambda-lambda):
updating: hello-world/hello-world (deflated 49%)
upload: hello-world/hello-world.zip to s3://<your-bucket>/hello-world.zip
Outputs:
Function: "arn:aws:lambda:us-west-2:ACCOUNTID:function:HelloWorldFunction"
Role ARN: "arn:aws:iam::ACCOUNTID:role/HelloWorldIAMRole-7532034"
Resources:
+ 3 created
Duration: 44s
Permalink: https://app.pulumi.com/retgits/lambda/lambda/updates/1
Testing with the AWS Console
Inside the Pulumi console, you’ll be able to see the resources that have been created.
Within the AWS Lambda console, you’ll be able to test the function and see that it indeed responds with the greeting “Hello, WORLD”.
Posted on January 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.