Skip to main content
How To Create AWS Lambda Functions Using Pulumi And Golang
  1. Blog/

How To Create AWS Lambda Functions Using Pulumi And Golang

·4 mins·
Infrastructure as Code with Pulumi and Go - This article is part of a series.
Part 6: This Article

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.go

Building 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"] = variables

Creating 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/1

Testing 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

Infrastructure as Code with Pulumi and Go - This article is part of a series.
Part 6: This Article

Related

Trusting your ingredients - What's in your function anyway?

·5 mins
As a developer, I’ve built apps and wrote code. As a cheesecake connoisseur, I’ve tried many different kinds of cheesecake. After I got to talk to some of the bakers, I realized that building apps and baking cheesecake have a lot in common. It all starts with knowing and trusting your ingredients. According to Tidelift, over 90 percent of applications contain some open source packages. Developers choose open source because they believe it’s better, more flexible, and more extendible. A lot of developers also fear how well packages are maintained and how security vulnerabilities are identified and solved.

Serverless - From Microservice to Functions

·1 min
Using serverless requires us to change our mindset on how we build apps and requires us to unlearn things we learned building apps in the past. At AWS re:Invent I got a chance to do a VMware Code session and talk about how we took part of our ACME Fitness Shop and transformed it into serverless functions with AWS Lambda.