Harnessing SAM for Local Lambda Development and Debugging
Written on
Serverless applications offer remarkable benefits, enabling rapid deployment of functions in production with exceptional uptime and minimal costs. AWS Lambda allows for the deployment of small, executable code snippets, such as Go binaries, which can be triggered through APIs, scheduled events, or other various triggers.
Debugging these cloud-based services can be quite challenging. I previously structured my functions to run locally, wrapping them with Lambda handlers. While this method works, there are times when a full flow debug is necessary, especially concerning the interconnected services involved.
I often emphasize the importance of local execution for software, and SAM (Serverless Application Model) facilitates this by allowing us to run cloud applications locally.
Before diving into this tutorial, ensure you have the following prerequisites: - An AWS account with sufficient IAM permissions. - The AWS CLI installed and configured.
If you lack an AWS account or the AWS CLI, please refer to the official documentation for installation guidelines.
For those who prefer video tutorials, you can find a visual representation of this article on my YouTube channel.
You can also access the complete code used in this video on GitHub.
Understanding SAM
SAM, or Serverless Application Model, is an AWS framework designed to simplify the creation of cloud applications. It features a template language that structures the cloud architecture and allows for local deployment and execution. SAM utilizes CloudFormation behind the scenes, but understanding CloudFormation is not necessary at this stage.
In this guide, we will create a few simple Lambdas and debug them using SAM, including attaching a remote debugger. SAM enables local execution of Lambdas or an API of Lambdas through Docker.
Using SAM is independent of any specific IDE; you can utilize any IDE of your preference. However, many developers tend to favor VS Code due to its excellent plugin, which we will explore later.
We will start by installing SAM, and you can find the official installation instructions on the AWS website.
I am using Linux, so here’s how I installed SAM.
You can verify the installation by executing the version command:
sam --version
Creating Your First Lambda
Let's create our initial simple Lambda function. In this tutorial, I'll demonstrate using Go, but you can apply any language that supports Lambda functions. You can find code examples for various languages in the AWS documentation, with sections labeled "Working with $LANGUAGENAME."
Create a new directory, initialize a Go module, and create two files: main.go and template.yml. For now, leave the template file blank; we will address it shortly.
mkdir sam-lambda-demo cd sam-lambda-demo go mod init programmingpercy.tech/sam-demo touch main.go touch template.yml
Next, let’s populate main.go with the simplest Lambda imaginable. This Lambda will accept an event containing a name and print Hello $name.
Yes, it’s a classic Hello World Lambda!
The Event struct defines the expected input for our Lambda, and we will return a string,error. You can return any struct or any of the AWS-defined events. We will cover AWS events shortly when we use the API Gateway in front of the Lambda.
Utilizing the SAM Template
Before we begin debugging, let’s cover some basics of SAM to understand what’s happening.
SAM includes a wealth of features, including the ability to deploy applications to AWS. For now, we'll take a step-by-step approach to build the Lambda and invoke it.
> When I refer to an Application, I mean the entire set of cloud resources, not just your single Go binary — it's the collection of resources that constitutes our Serverless Application.
To build our application, we first need to define the cloud resources it will utilize. This is specified in the template.yml file using a defined format.
We will start with the simplest template I can envision and gradually explain each component.
At the beginning of the file, you will see some default settings that typically remain unchanged. We specify the template version here. According to the documentation, there is only one valid version.
Our focus will be on the Resources section, where we can define the resources for our entire SAM application.
The syntax is straightforward: begin the YAML file with the resource name. For example, we will create a Lambda named HelloGopher. This name can be anything you choose, but remember that you will reference this name in other resources, which is vital when you need a specific ARN, etc.
All resources require a Type input, which can be any type supported by CloudFormation. Generally, it follows the format AWS::Serverless:RESOURCETYPE. Here, we set the type to AWS::Serverless::Function, instructing CloudFormation to create a Lambda for us.
Each type has its own set of properties, which you can review in the documentation. The CodeUri property is essential; it indicates the local path or the S3 path to the ZIP file containing our code. In this instance, I’m using the current folder, but in a real project, it’s advisable to create a structured folder layout like lambdas/hello-gopher/ and adjust the CodeUri accordingly.
The Handler property specifies the binary that will be executed when the Lambda is triggered. Let’s utilize SAM to clarify this.
If you try to run the Lambda now using sam local invoke, you should encounter a crash report indicating that the file is missing.
This occurs because we've set the Handler to point to the hello-gopher binary, which does not yet exist. Let’s start employing SAM to assist us.
SAM Build and Invoke
We can use SAM to package our application; the build command comes with various parameter flags. For instance, you can build it and send it to S3.
Using sam build is beneficial as it utilizes your template file and the appropriate Go version based on the runtime property.
It's quite simple. In the same folder as your template.yml, run the following command:
sam build
A .aws folder should now appear, and within it, you will find the Lambda and the hello-gopher binary.
Now, let’s attempt to run the project again by locally invoking the Lambda.
sam local invoke
You should see it printing "Hello," but without a name. This is because we need to provide the input event. You can do this using the -e or --event option, which can reference a file or a JSON string. I prefer using files, as they also serve as documentation for the Lambda as a sample.
Create a file in the folder named event.json and insert JSON that corresponds to the Event struct in the Lambda.
Now, invoke the Lambda again, but this time include the -e flag pointing to the event.json file.
Fantastic! Now it prints the name we provided in the payload. There are numerous custom Lambda events available in the AWS SDK, but we will explore those in a dedicated Lambda tutorial.
There’s also a -d flag that allows us to specify a remote debug port, which is crucial to remember. This feature enables us to attach a debugger to the Lambda.
Although we only have one Lambda, if you have multiple, you can specify which Lambda to run by appending the name of the Lambda to the command.
sam local invoke hello-gopher # Runs a specific Lambda
If you want to run a Lambda and expose it as a service, simulating its operation in the cloud, you can do so using the start-lambda command.
Let’s execute the following command:
sam local start-lambda
This should output a URL where the Lambda is exposed, which you can use with the AWS CLI to invoke the Lambda. The default URL is http://127.0.0.1:3001.
You can use this URL as the endpoint in the AWS CLI to invoke it with the following command:
aws lambda invoke --function-name HelloGopher --endpoint "http://127.0.0.1:3001" --payload '{ "name": "percy"}' response.json
This command will invoke the Lambda, provide the payload, and save the response to response.json.
Integrating SAM with API Gateway
Often, you may want to run your Lambda with an API Gateway in front of it. The API Gateway exposes the Lambda as an HTTP API.
Setting this up is straightforward with SAM. We need to create Events for the Lambda resource to listen for. This doesn’t have to be HTTP; it can also be SQS or other AWS service events. Specifying the type as API informs SAM that this is an API Gateway.
We will modify the template.yml to include the API endpoint as a POST request.
To open the API and expose the endpoints locally—an extremely useful feature when developing an API with multiple endpoints—we can utilize SAM once more.
Let’s build the new Lambda and run the API with:
sam build sam local start-api
You should see an output that indicates which port the API is running on; for example:
Mounting HelloGopher at http://127.0.0.1:3000/api/hellogopher [POST]
We can test this using CURL by sending the expected data payload:
curl -X POST localhost:3000/api/hellogopher -d '{"name": "percy"}'
Managing Environment Variables with SAM
Often, your Lambda will require configuration, typically handled using environment variables.
I recommend defining the expected variables in the template.yml, allowing for modification through the CLI, which we will discuss shortly.
To add environment variables, we adjust the template and include an Environment property.
Here’s a snippet of my template.yml featuring an environment variable named my-cool-variable.
Next, we need to incorporate this environment variable into the Lambda. I will simply add its value to the output by modifying line 31 in main.go.
This may seem trivial, but you can now effectively leverage this in the SAM CLI to set new variables. We can modify variables between invocations using the -n parameter. Remember, you can have multiple Lambda endpoints in the API, and each Lambda can require its own set of environment variables.
You can create a dedicated environment JSON file that manages each Lambda’s environment variables. Use the resource name from the template.yml, and only environments specified there will be modifiable. If you attempt to set a variable not declared in the template, it will not be added.
Create a new file named environments.json, which we will use to modify each Lambda resource's environment variables.
Try rebuilding and executing the API using the -n flag to indicate the environment file, and you should see the new value printed.
sam local start-api -n environments.json
There are also parameters, which differ from environment variables, relating more closely to CloudFormation, but we won’t delve into those details here.
Generating Events with SAM
Not all Lambdas are triggered by APIs; some listen for SQS events or S3 events, for example.
A common scenario is having a Lambda respond to SQS events, although testing this can be more complex. SAM cannot connect directly to an SQS queue for testing. Instead, you generate an SQS payload and invoke the Lambda with that payload, simulating a live event being triggered.
You might wonder what an SQS event looks like. Fortunately, you don’t need to know the specifics because SAM can generate dummy payloads for well-known AWS services typically related to Lambdas.
Let’s begin by updating the template.yml to include a new Lambda that listens for SQS events on a queue named my-awesome-queue. Since we can’t have SAM listen on my-awesome-queue locally, we will simulate the payload. The following example demonstrates how to tell SAM about SQS; the only new aspect in the template.yml is the Lambda that triggers on SQS events.
In the CodeUri, we specify the ./sqslambda location. Let’s create that folder and add a main.go file within it.
mkdir ./sqslambda touch main.go
Our new Lambda will be quite simple, only logging the incoming event. We’ll use the events package from the AWS SDK and declare the event as an SQSEvent. Instead of manually copying the SQSEvent structure using JSON tags, we can streamline the process.
Before triggering this Lambda, we need an SQSEvent. We can use the generate-event command to create an event.json file, which we can use as payload.
The syntax is straightforward: use generate-event, followed by the service name and the receive-message subcommand. Currently, there’s only one subcommand, receive-message.
We can pass an argument in --body to modify the body of the SQS payload, which represents the user-specific payload.
sam local generate-event sqs receive-message --body 'My Own Event Payload'
Executing this command will generate a JSON payload that mimics an SQS event. You can either write this payload into a file named event.json and pass it as before or utilize a neat trick: if you pass a single - as input to the -e flag, it will read values from STDIN. This allows us to chain the generate event command with the invoke command.
sam local generate-event sqs receive-message --body 'My Own Event Payload' | sam local invoke -e - SQSLambda
Running that command will print the entire event and invoke the Lambda.
Excellent! We can now test any Lambda, regardless of its triggering service.
Attaching a Remote Debugger
Debugging is essential when developing a service, and attaching a debugger to observe runtime behavior is one of the most effective methods for resolving issues.
To achieve this, we can add the -d flag to SAM commands to open a remote debugger port, specifying the port as an argument to -d. This feature is available for all invocations, including start-lambda, start-api, and invoke.
> Note: SAM requires the Linux Delve debugger to be installed. If you are not using Linux, you can still install it via the command: GOOS=linux GOARCH=amd64 go install github.com/go-delve/delve/cmd/dlv@latest.
Ensure the debugger is installed on your host, and you can specify its location using the --debugger-path=hostURL/to/debugger argument. Additionally, I recommend using the Delve API version 2 for smoother operation.
Let’s run the SQS event to debug the Lambda. I will include the necessary debug flags to expose the Lambda in debug mode. Remember, when invoking with -d, it will pause the Lambda at the start, waiting for a debugger to attach.
sam local invoke SQSLambda -d 8099 --debugger-path=/home/percy/go/bin --debug-args="-delveAPI=2"
Next, we need to attach a debugger. The method for this will vary based on whether you’re using VS Code, Goland, or another IDE. I am using VS Code, so I will add a new configuration to my .vscode/launch.json. If you are using Goland, please refer to their documentation on attaching a debugger.
Essentially, we create a new attach request for a remote debugger at localhost:8099. Ensure you use the same port you provided in the -d command.
Save the file and place a breakpoint in sqslambda/main.go on line 13. Then run the debug configuration. You should see the execution pause at your set breakpoint.
While this setup requires some effort, there are ways to automate the execution of the debugger command using preLaunch effects in the launch.json. However, if you are using VS Code, I will soon cover how to utilize the AWS plugin, which streamlines debugging by eliminating the need for CLI commands.
Integrating VS Code with SAM
If you are using VS Code, I highly recommend downloading the AWS Toolkit if you haven’t done so already.
Go to the extensions section, search for AWS Toolkit, and install it.
After installing, open the extension and log in to your desired profile by clicking the Connect to AWS button.
The process should be straightforward, requiring a few configurations like the default region and profile. Select your preferred options, and you should see various cloud services listed.
While we won’t be deploying any SAM applications in this tutorial, the plugin adds numerous built-in features to replace command-line operations. The most notable feature is that each Lambda will display a prompt asking if you want to create a debugging configuration.
Click on Add Debug Configuration, and your launch.json will be updated.
This configuration will differ significantly from the one we created earlier. This is one of the advantages of the plugin; it generates a unique debug type called aws-sam, enabling us to invoke Lambdas easily. There’s also a request type for API calls, which you can generate by accessing the API Lambda and creating a debug call for it.
You can specify the invoke target, environment variables, and the payload. This convenience allows us to bypass the API and simply use a streamlined launch.json.
You should recognize the various options available, as they correspond to what we’ve covered in this article.
So, why didn’t we utilize the plugin from the outset? Because doing so would have obscured the underlying mechanics, preventing us from grasping what the plugin accomplishes. I believe that understanding the foundational tools is the most beneficial approach in the long run. Additionally, you’re not restricted to using VS Code; you can employ any IDE you prefer, as you now understand how to attach a debugger and why it’s necessary.
Setting Up Localstack and SAM Network
If your Lambdas interact with other AWS resources, you can simulate these using Localstack. While I won’t cover Localstack in this tutorial, I will explain how to configure SAM resources to operate on the same network as a Localstack instance.
When running Localstack via Docker, ensure you specify a Docker network. If your Localstack instance runs on a Docker network named mock-aws-network, you can make SAM utilize the same network by adding the --docker-network mock-aws-network flag to most commands.
sam local invoke --docker-network mock-aws-network sam local start-api --docker-network mock-aws-network
This is particularly useful, as Lambdas often rely on other AWS services that need to communicate, facilitating debugging as well.
If you’re using the VS Code plugin, you can incorporate this line into your launch.json.
Conclusion
In this tutorial, we explored how to develop Lambdas using SAM and debug them locally. SAM significantly eases the Lambda development process, alleviating many frustrations I faced when starting with serverless applications. Tasks like debugging, once difficult, are now manageable.
There are still more SAM features I haven’t covered here, such as creating additional AWS resources (SQS queues, etc.) and referencing their ARNs directly in your Lambda.
I encourage you to experiment with SAM and discover all the powerful tools it offers.
Thank you for reading! As always, feel free to reach out; I welcome feedback and discussions.