DEV Community

Leon Stigter
Leon Stigter

Posted on • Originally published at retgits.com on

How To Create AWS Lambda Functions Using Pulumi And Golang

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)
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

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())

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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())

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Testing with the AWS Console

Inside the Pulumi console, you’ll be able to see the resources that have been created.

The Pulumi console also has really useful links to the AWS console to see the resources.

The Pulumi console also has really useful links to the AWS console to see the resources.

Within the AWS Lambda console, you’ll be able to test the function and see that it indeed responds with the greeting “Hello, WORLD”.

The AWS Lambda console.

The AWS Lambda console.

Top comments (0)