Golang API on AWS Lambda with Cognito authentication
How to build a serverless web service using SAM template, Lambda, DynamoDb and Go-lang on AWS
This article is part of an aggregating article describing Serverless React/Gatsby/Go template
Server, container or serverless
When you develop a service layer for your project, the first question that you want to answer is if you want to run it in a fleet of servers, build a container-based solution or you want to build a serverless solution.
There may be many considerations here, we’ll outline some of them:
- If you based your API on a server architecture, be ready to deal with fleet scaling, host replacements, OS upgrades, etc. You will also need to have a load balancer in your infrastructure as well as VPC, NAT and other components. This way you introduce a lot of complexity to your infrastructure, as well as to your deployment (CI/CD) story. This is where serverless solutions based on AWS Lambda or GCP Cloud Functions have lots of benefits.
- At the same time, serverless solutions may introduce a significant cold start delay. Cold start happens when your function logic is being provisioned by Lambda infrastructure. For example, when you deploy a new version, or when it wasn’t used for a while. Lambda infrastructure has a logic to shut down services that were not used for ~20 minutes. The cold start problem varies from language to language, it also depends on the instance size, binary size, dependencies, etc. For example, cold start may be nasty on Lambdas running Java (due to JVM), especially if it loads some large dependencies (like Spring). This is something everyone developing for Lambda should keep in mind.
- Costwise, serverless solutions will be more efficient if your load is tiny, whereas server and container solutions may become cheaper on a significant amount of traffic.
In this template we use a Serverless approach in the API development, deploying the service logic to AWS Lambda.
It’s important to mention that we logically separate the backend code. So, if in the future you decide to move to containers, it will affect only your backend code, but not the other parts of your infrastructure.
At this moment AWS Lambda supports the following languages: Node.js, Python, Ruby, Java, Go, .NET.
In this template we used Node.js for the authentication logic and Go for other backend services.
We choose Go for the following reasons:
- Performance of a compiled language
- Strongly typed language
- Cold start is better than in pre-compiled languages, like Java or .NET
- Good community support
As was mentioned before, we separated the backend code, as well as its infrastructure from the frontend logic. Such separation gives us a lot of flexibility due to decoupling from the front-end code. However, it comes with some inconveniences.
The major inconvenience is the necessity of handling Cross-origin resource sharing (CORS) due to the fact that API requests come from a different domain name.
If such cross-domain requests are not handled properly you will see various errors:
If your Lambda function doesn’t handle
OPTIONS requests, you will just see
OPTIONS request returning 403 error code in your browser.
Then, it’s important that Lambda returns proper headers in every response, otherwise you’ll see something like this in your browser:
XMLHttpRequest cannot load https://api.example.com. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://example.com' is therefore not allowed access
The template discussed in this article sets up proper API Gateway endpoints to handle OPTIONS requests, as well as sets up proper headers for smooth frontend-backend communication.
Running the backend locally
Lambda functions in this template are defined using SAM template. It allows you to build, test and run a copy of the cloud infranstructure locally on your computer without a need to re-deploy them on every change.
You can run the backend locally by executing the following command:
This command will call
sam local start-api, which in turn will download the Lambda function docker container and run your infrastructure in it.
no such file or directory: PathError null
If your Lambda function calls fail with Server error, and you see the following error in the CloudWatch log
fork/exec /var/task/main: no such file or directory: PathError null
then it means that the actual code compiled by Go is not delivered to the Lambda function.
To troubleshoot, first, make sure that you build the binaries correctly:
GOOS=linux go build -o main .
Second, make sure that you build all golang files, and put the output to the right place. For example, we use to following command to build all golang files serving as Lambda entry points:
for filename in handlers/*/main.go; do echo Building $filename at $(dirname $filename) GOOS=linux go build -ldflags="-s -w" -o $(dirname $filename)/main $filename; done
Artifact [BuildArtifact] exceeds max artifact size
Code Pipelines may complain about artifact being too big:
Artifact [BuildArtifact] exceeds max artifact size
This happens when build artifacts from a previous build step exceeds some threshold. We don’t know the exact value of the threshold, it’s somewhere near 200Mb. This threshold can be increased, you need to contact AWS support to do this.
Another way to approach this problem is to try to decrease the artifact size. In our template we are using the
-ldflags="-s -w" flags while building Go code.
The -w turns off DWARF debugging information: you will not be able to use GNU Project Debugger (gdb) on the binary to look at specific functions or set breakpoints or get stack traces, because all the metadata gdb needs will not be included.
The -s turns off generation of the Go symbol table. you will not be able to use ‘go tool nm’ to list the symbols in the binary.
Another reason for using these flags is that by decreasing the binary size you improve Lambda’s cold start time, which is important for users facing APIs.