Goa is a powerful way to to build REST API backends in Go using it’s powerful design langugage and OpenAPI Spec generation capabilities.
It’s possible to deploy your Goa backend on AWS Lambda, with help from eawsy/aws-lambda-go-shim and aws-lambda-go-net.
This guide walks you through the entire process.
Note
|
You might want to check the latest instructions, in case these are out of date. |
mkdir serverless-forms; cd serverless-forms
Replace serverless-forms
with your own project name.
This assumes you have Go 1.8 installed.
docker pull eawsy/aws-lambda-go-shim:latest
go get -u -d github.com/eawsy/aws-lambda-go-core/...
wget -O Makefile https://git.io/vytH8
Create a new file handler.go
in your project directory with the following content:
package main
import (
"encoding/json"
"github.com/eawsy/aws-lambda-go-core/service/lambda/runtime"
)
func Handle(evt json.RawMessage, ctx *runtime.Context) (interface{}, error) {
return "Hello, World!", nil
}
This is the function that will be called back by AWS Lambda (through the shim)
Run make:
make
and now you should have a new file called handler.zip
$ ls -alh handler.zip
-rw-r--r--@ 1 tleyden staff 1.5M Jun 4 10:20 handler.zip
Note
|
you can also do this manually via the AWS Web UI, and if you’ve already created an AWS Lambda function before, you already have this role and can skip this step. |
cat > trust-policy.json <<EOL
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}]
}
EOL
aws iam create-role --role-name lambda_basic_execution --assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy --role-name lambda_basic_execution --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Find your AWS account number from the AWS Web Admin, and replace 19382281 below with your AWS account number.
AWS_ACCOUNT_NUMBER=19382281
Deploy the Lambda function:
aws lambda create-function \
--role arn:aws:iam::$AWS_ACCOUNT_NUMBER:role/lambda_basic_execution \
--function-name preview-go \
--zip-file fileb://handler.zip \
--runtime python2.7 \
--handler handler.Handle
At this point, your Lambda function is deployed, but it is not yet accessible via a REST API call. Putting it behind the AWS API Gateway via eawsy/aws-lambda-go-net exposes a REST API endpoint.
Note
|
The latest version of these docs is available on the eawsy/aws-lambda-go-net |
package main
import (
"net/http"
"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
)
// Handle is the exported handler called by AWS Lambda.
var Handle apigatewayproxy.Handler
func init() {
ln := net.Listen()
// Amazon API Gateway binary media types are supported out of the box.
// If you don't send or receive binary data, you can safely set it to nil.
Handle = apigatewayproxy.New(ln, []string{"image/png"}).Handle
// Any Go framework complying with the Go http.Handler interface can be used.
// This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, Goa, etc.
go http.Serve(ln, http.HandlerFunc(handle))
}
func handle(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
Create a new file named aws_serverless_application_model.yaml
with the following content:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
Handler: handler.Handle
Runtime: python2.7
CodeUri: ./handler.zip
Events:
ApiRoot:
Type: Api
Properties:
Path: /
Method: ANY
ApiGreedy:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
Outputs:
URL:
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
Create a new S3 bucket which will hold your packaged cloudformation templates.
$ aws s3api create-bucket --bucket my-bucket
$ S3_BUCKET="my-bucket"
Note
|
see aws s3api docs, this might need more parameters. |
Upload the packaged cloudformation template to s3:
aws cloudformation package \
--template-file aws_serverless_application_model.yaml \
--output-template-file aws_serverless_application_model.out.yaml \
--s3-bucket $S3_BUCKET
Choose a name for your cloudformation stack
CLOUDFORMATION_STACK_NAME="HelloServerlessGolangApi"
Deploy the cloudformation stack
aws cloudformation deploy \
--template-file aws_serverless_application_model.out.yaml \
--capabilities CAPABILITY_IAM \
--stack-name $CLOUDFORMATION_STACK_NAME \
--region us-east-1
Find out the URL of the API Gateway endpoint via Cloudformation Template outputs:
aws cloudformation describe-stacks \
--stack-name $CLOUDFORMATION_STACK_NAME \
--query Stacks[0].Outputs[0]
This will give you a URL like:
------------------------------------------------------------------------------
| DescribeStacks |
+-----------+----------------------------------------------------------------+
| OutputKey | OutputValue |
+-----------+----------------------------------------------------------------+
| URL | https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod |
+-----------+----------------------------------------------------------------+
Now try to issue a curl request against it:
$ curl https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod
Hello, World!
package design
import (
. "github.com/goadesign/goa/design"
. "github.com/goadesign/goa/design/apidsl"
)
var _ = API("HelloServerlessGoa", func() {
Title("Goa Server API Example")
Description("Goa API powered by AWS Lambda and API Gateway")
Scheme("http")
Host("localhost:8080")
})
var _ = Resource("hello", func() {
BasePath("/hello")
DefaultMedia(HelloMedia)
Action("show", func() {
Description("Say Hello")
Routing(GET("/:whatToSay"))
Params(func() {
Param("whatToSay", String, "What To Say Hello To")
})
Response(OK)
Response(NotFound)
})
})
var HelloMedia = MediaType("application/vnd.hello+json", func() {
Description("Hello World")
Attributes(func() {
Attribute("hello", String, "What was said")
Required("hello")
})
View("default", func() {
Attribute("hello")
})
})
Generate the controller, which we will customize:
goagen controller --force --pkg controller -d github.com/tleyden/serverless-forms/design -o ./controllers
and the remaining goa generated code, which we won’t touch.
goagen app -d github.com/tleyden/serverless-forms/design -o ./goa-generated
goagen client -d github.com/tleyden/serverless-forms/design -o ./goa-generated
goagen swagger -d github.com/tleyden/serverless-forms/design -o ./goa-generated
Generate the main
scaffolding:
goagen main -d github.com/tleyden/serverless-forms/design
and remove the hello.go
which we don’t need, since it’s already in the controllers
directory
rm hello.go
Sorry, this part is really ugly, I need to get in touch with the goa folks to try to make this cleaner. Part of the issue is that I’m putting everything in the goa-generated
directory, to keep the generated code separate, which breaks the package names.
-
Open
main.go
and-
Change the
app
package import togoa-generated/app
-
Add this package import:
controller "github.com/tleyden/serverless-forms/controllers"
-
Change
c := NewHelloController(service)
→c := controller.NewHelloController(service)
-
-
Open
controllers/hello.go
and change theapp
package import togoa-generated/app
go run main.go
and you should see output:
2017/06/04 12:32:00 [INFO] mount ctrl=Hello action=Show route=GET /hello/:whatToSay
2017/06/04 12:32:00 [INFO] listen transport=http addr=:8080
and if you curl:
$ curl localhost:8080/hello/foo
{"hello":""}
Open controllers/hello.go
and look for this line:
res := &app.Hello{}
and add a new line, so it’s now:
res := &app.Hello{}
res.Hello = ctx.WhatToSay
Now return the goa api server via go run main.go
, and retry that curl request:
$ curl localhost:8080/hello/world
{"hello":"world"}
and it now echos the parameter passed along the request path.
At this point there are two files that need to have their functionality merged:
-
handler.go
 — this contains the Lambda / API Gateway stub code that was previously pushed up to AWS in a previous step -
main.go
 — this contains the goa REST API server
handler.go
is deleted and it’s functionality gets merged into main.go
after some minor refactoring.
//go:generate goagen bootstrap -d github.com/tleyden/serverless-forms/design
package main
import (
"net/http"
"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
"github.com/goadesign/goa"
"github.com/goadesign/goa/middleware"
controller "github.com/tleyden/serverless-forms/controllers"
"github.com/tleyden/serverless-forms/goa-generated/app"
)
func createGoaService() *goa.Service {
// Create service
service := goa.New("HelloServerlessGoa")
// Mount middleware
service.Use(middleware.RequestID())
service.Use(middleware.LogRequest(true))
service.Use(middleware.ErrorHandler(service, true))
service.Use(middleware.Recover())
// Mount "hello" controller
c := controller.NewHelloController(service)
app.MountHelloController(service, c)
return service
}
func main() {
service := createGoaService()
// Start service
if err := service.ListenAndServe(":8080"); err != nil {
service.LogError("startup", "err", err)
}
}
// Handle is the exported handler called by AWS Lambda.
var Handle apigatewayproxy.Handler
func init() {
ln := net.Listen()
// Amazon API Gateway Binary support out of the box.
Handle = apigatewayproxy.New(ln, nil).Handle
service := createGoaService()
// Any Go framework complying with the Go http.Handler interface can be used.
// This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, etc.
go http.Serve(ln, service.Mux)
}
Re-run the same steps previously mentioned in Deploy aws-lambda-go-shim behind API Gateway
$ make
$ aws cloudformation package \
--template-file aws_serverless_application_model.yaml \
--output-template-file aws_serverless_application_model.out.yaml \
--s3-bucket $S3_BUCKET
$ aws cloudformation deploy \
--template-file aws_serverless_application_model.out.yaml \
--capabilities CAPABILITY_IAM \
--stack-name $CLOUDFORMATION_STACK_NAME \
--region us-east-1
$ aws cloudformation describe-stacks \
--stack-name $CLOUDFORMATION_STACK_NAME \
--query Stacks[0].Outputs[0]