I’ve used Pulumi to do a bunch of things so far: creating subnets in a VPC, building EKS clusters, and DynamoDB tables. The one thing I hadn’t tried yet was deploying Lambda functions, so that’s what this post covers.
The complete project is available on GitHub.
My Lambda #
The Lambda function here is straightforward — it reads an environment variable and says hello:
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 lives in a file called hello-world.go inside a hello-world folder. The Pulumi project sits in its own pulumi folder so it doesn’t collide with your Lambda code. The folder structure looks like this:
├── README.md
├── go.mod
├── go.sum
└── hello-world
│ └── main.go
├── pulumi
│ ├── Pulumi.lambdastack.yaml
│ ├── Pulumi.yaml
└── main.goBuilding and uploading your Lambda code #
To deploy a Lambda function, the code needs to be packaged and uploaded. Pulumi has an Archive concept for creating zip files, but the Go implementation has a known issue that makes it unusable. Instead, you can extend the Pulumi program to handle the build, zip, and upload steps before the main run:
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 runs a shell command and returns an error or nil. These three calls build the binary, zip it, and upload it to S3. Place them before pulumi.Run():
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 any of these fail, you’ll see the output and error message in the diagnostics section of your terminal.
Creating an IAM role #
Every Lambda function needs an IAM role to operate. This one just needs permission to run. The ARN (Amazon Resource Name) is exported so it’s visible in 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 #
The Pulumi SDK lets you define environment variables, just like CloudFormation. Since the Lambda function reads a NAME variable, you set it up as a nested map:
environment := make(map[string]interface{})
variables := make(map[string]interface{})
variables["NAME"] = "WORLD"
environment["variables"] = variablesCreating the function #
The last step is creating the Lambda function itself. The S3Bucket and S3Key point to the zip file you uploaded earlier, and role.Arn() references the IAM role:
// 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 everything in place, run pulumi up to deploy. If you need details on setting up 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/1Testing with the AWS Console #
In the Pulumi console, you can see the resources that were created:

In the AWS Lambda console, you can test the function and confirm it responds with “Hello, WORLD”:

Cover image by Kevin Horvat on Unsplash