I wanted to compile a binary which contains a whole Go web app and is deployed as a Lambda function to AWS. This function (or binary) should also have a cache from CloudFront in front of it. Using the newly introduced stale-while-revalidate support from CF, this app should be very fast without waiting for a cold start: Instead of waiting for a cold start, a cache entry is responded while a new response is retrieved from the origin in the background.
To make development easy, I also have a cli under cmd/rvweb
which gives me a local binary to start a webserver with the handler from http.go
. When deploying, this same handler is exposed as a Lambda handler and deployed.
|-- Makefile
|-- cdk
| |-- README.md
| |-- cdk.go
| |-- cdk.json
| |-- cdk.out
| |-- cdk_test.go
| |-- go.mod
| `-- go.sum
|-- cmd
| `-- rvweb
|-- go.mod
|-- go.sum
|-- http.go
`-- lambda
`-- lambda.go
My example handler can be found in http.go
which just returns a string and cache headers. With the values I used here (max-age=60, stale-while-revalidate=60
) CloudFront will cache for a minute. And now, quoting from the AWS docs: “If a request is made after this period, CloudFront serves the stale content while concurrently sending a request to the new origin. The stale content is served for up to one minute while the content is being revalidated.”
package rvweb
import (
"net/http"
"github.com/labstack/echo/v4"
)
func HttpServer() *echo.Echo {
e := echo.New()
e.GET("/", func(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=60")
return c.String(http.StatusOK, "Hello, World3")
})
return e
}
It returns *echo.Echo
which embeds a http.Server
. The cmd (or binary) package can now import this code and just start a web server for local development.
The Lambda handler is exposed via the lambda/lambda.go
function:
package main
import (
"github.com/akrylysov/algnhsa"
"github.com/rverton/rvweb"
)
func main() {
algnhsa.ListenAndServe(rvweb.HttpServer(), nil)
}
This gives me an easy way to expose the same http router/handler to a local dev server and to a Lambda function.
CDK (with Go) is then used to deploy the function with a Lambda function URL. I found a nice working stack here which I slightly adjusted to use caching.
// [...]
func NewCdkStack(scope constructs.Construct, id string, props *CdkStackProps) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
bundlingOptions := &awslambdago.BundlingOptions{
GoBuildFlags: &[]*string{jsii.String(`-ldflags "-s -w" -tags lambda.norpc`)},
}
f := awslambdago.NewGoFunction(stack, jsii.String("handler"), &awslambdago.GoFunctionProps{
Runtime: awslambda.Runtime_PROVIDED_AL2(),
MemorySize: jsii.Number(128),
Architecture: awslambda.Architecture_ARM_64(),
Entry: jsii.String("../lambda"),
Bundling: bundlingOptions,
})
// Add a Function URL.
lambdaURL := f.AddFunctionUrl(&awslambda.FunctionUrlOptions{
AuthType: awslambda.FunctionUrlAuthType_NONE,
})
awscdk.NewCfnOutput(stack, jsii.String("lambdaFunctionUrl"), &awscdk.CfnOutputProps{
ExportName: jsii.String("lambdaFunctionUrl"),
Value: lambdaURL.Url(),
})
lambdaURLDomain := awscdk.Fn_Select(jsii.Number(2), awscdk.Fn_Split(jsii.String("/"), lambdaURL.Url(), nil))
lambdaOrigin := awscloudfrontorigins.NewHttpOrigin(lambdaURLDomain, &awscloudfrontorigins.HttpOriginProps{
ProtocolPolicy: awscloudfront.OriginProtocolPolicy_HTTPS_ONLY,
})
cf := awscloudfront.NewDistribution(stack, jsii.String("customerFacing"), &awscloudfront.DistributionProps{
DefaultBehavior: &awscloudfront.BehaviorOptions{
AllowedMethods: awscloudfront.AllowedMethods_ALLOW_ALL(),
Origin: lambdaOrigin,
CachedMethods: awscloudfront.CachedMethods_CACHE_GET_HEAD(),
OriginRequestPolicy: awscloudfront.OriginRequestPolicy_ALL_VIEWER_EXCEPT_HOST_HEADER(),
CachePolicy: awscloudfront.CachePolicy_CACHING_OPTIMIZED(),
ViewerProtocolPolicy: awscloudfront.ViewerProtocolPolicy_REDIRECT_TO_HTTPS,
},
PriceClass: awscloudfront.PriceClass_PRICE_CLASS_100,
})
awscdk.NewCfnOutput(stack, jsii.String("cloudFrontDomain"), &awscdk.CfnOutputProps{
ExportName: jsii.String("cloudfrontDomain"),
Value: cf.DomainName(),
})
return stack
}
// [...]
To make development and deployment easier, I’m using a Makefile
which holds some nice tasks for testing, cleaning, building, running, watching and deploying. Most of the basic functionality can be found here.