Goa is a powerful way to to build REST API backends in Go using it’s powerful design langugage and OpenAPI Spec generation capabilities.
477 |It’s possible to deploy your Goa backend on AWS Lambda, with help from eawsy/aws-lambda-go-shim and aws-lambda-go-net.
480 |This guide walks you through the entire process.
483 |Installation
488 |Deploy aws-lambda-go-shim
491 |
495 | Note
496 | |
497 | 498 | You might want to check the latest instructions, in case these are out of date. 499 | | 500 |
Create a project directory
505 |mkdir serverless-forms; cd serverless-forms
508 | Replace serverless-forms
with your own project name.
Get dependencies
516 |This assumes you have Go 1.8 installed.
518 |docker pull eawsy/aws-lambda-go-shim:latest
522 | go get -u -d github.com/eawsy/aws-lambda-go-core/...
523 | wget -O Makefile https://git.io/vytH8
524 | Add lambda function handler
529 |Create a new file handler.go
in your project directory with the following content:
package main
535 |
536 | import (
537 | "encoding/json"
538 |
539 | "github.com/eawsy/aws-lambda-go-core/service/lambda/runtime"
540 | )
541 |
542 | func Handle(evt json.RawMessage, ctx *runtime.Context) (interface{}, error) {
543 | return "Hello, World!", nil
544 | }
545 | This is the function that will be called back by AWS Lambda (through the shim)
549 |Build handler.zip
553 |Run make:
555 |make
559 | and now you should have a new file called handler.zip
$ ls -alh handler.zip
567 | -rw-r--r--@ 1 tleyden staff 1.5M Jun 4 10:20 handler.zip
568 | Create AWS Lambda IAM Role
573 |
577 | Note
578 | |
579 | 580 | 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. 581 | | 582 |
cat > trust-policy.json <<EOL
588 | {
589 | "Version": "2012-10-17",
590 | "Statement": [{
591 | "Effect": "Allow",
592 | "Principal": {
593 | "Service": "lambda.amazonaws.com"
594 | },
595 | "Action": "sts:AssumeRole"
596 | }]
597 | }
598 | EOL
599 |
600 | aws iam create-role --role-name lambda_basic_execution --assume-role-policy-document file://trust-policy.json
601 | aws iam attach-role-policy --role-name lambda_basic_execution --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
602 | Deploy to AWS Lambda
607 |Find your AWS account number from the AWS Web Admin, and replace 19382281 below with your AWS account number.
609 |AWS_ACCOUNT_NUMBER=19382281
613 | Deploy the Lambda function:
617 |aws lambda create-function \
621 | --role arn:aws:iam::$AWS_ACCOUNT_NUMBER:role/lambda_basic_execution \
622 | --function-name preview-go \
623 | --zip-file fileb://handler.zip \
624 | --runtime python2.7 \
625 | --handler handler.Handle
626 | Verify
631 |-
633 |
-
634 |
In the AWS Web Admin, go to the Lambda section
635 |
636 | -
637 |
Choose the
638 |preview-go
lambda function
639 | -
640 |
Under Actions, select Test Function
641 |
642 | -
643 |
Hit the Save and Test button
644 |
645 | -
646 |
Under "The area below shows the result returned by your function execution.", you should see "Hello World!" — this means it worked!
647 |
648 |
Deploy aws-lambda-go-shim behind API Gateway
654 |
658 | Note
659 | |
660 | 661 | The latest version of these docs is available on the eawsy/aws-lambda-go-net 662 | | 663 |
Get dependencies
668 |go get -u -d github.com/eawsy/aws-lambda-go-net/...
671 | Update handler.go
676 |package main
679 |
680 | import (
681 | "net/http"
682 |
683 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
684 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
685 | )
686 |
687 | // Handle is the exported handler called by AWS Lambda.
688 | var Handle apigatewayproxy.Handler
689 |
690 | func init() {
691 | ln := net.Listen()
692 |
693 | // Amazon API Gateway binary media types are supported out of the box.
694 | // If you don't send or receive binary data, you can safely set it to nil.
695 | Handle = apigatewayproxy.New(ln, []string{"image/png"}).Handle
696 |
697 | // Any Go framework complying with the Go http.Handler interface can be used.
698 | // This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, Goa, etc.
699 | go http.Serve(ln, http.HandlerFunc(handle))
700 | }
701 |
702 | func handle(w http.ResponseWriter, r *http.Request) {
703 | w.Write([]byte("Hello, World!"))
704 | }
705 | Rebuild handler.zip
710 |make
713 | Create SAML (AWS Serverless Application Model) file
718 |Create a new file named aws_serverless_application_model.yaml
with the following content:
AWSTemplateFormatVersion: '2010-09-09'
724 | Transform: AWS::Serverless-2016-10-31
725 | Resources:
726 | Function:
727 | Type: AWS::Serverless::Function
728 | Properties:
729 | Handler: handler.Handle
730 | Runtime: python2.7
731 | CodeUri: ./handler.zip
732 | Events:
733 | ApiRoot:
734 | Type: Api
735 | Properties:
736 | Path: /
737 | Method: ANY
738 | ApiGreedy:
739 | Type: Api
740 | Properties:
741 | Path: /{proxy+}
742 | Method: ANY
743 | Outputs:
744 | URL:
745 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
746 | Create an S3 bucket
751 |Create a new S3 bucket which will hold your packaged cloudformation templates.
753 |$ aws s3api create-bucket --bucket my-bucket
757 | $ S3_BUCKET="my-bucket"
758 |
764 | Note
765 | |
766 | 767 | see aws s3api docs, this might need more parameters. 768 | | 769 |
Deploy to AWS Lambda
775 |Upload the packaged cloudformation template to s3:
777 |aws cloudformation package \
781 | --template-file aws_serverless_application_model.yaml \
782 | --output-template-file aws_serverless_application_model.out.yaml \
783 | --s3-bucket $S3_BUCKET
784 | Choose a name for your cloudformation stack
788 |CLOUDFORMATION_STACK_NAME="HelloServerlessGolangApi"
792 | Deploy the cloudformation stack
796 |aws cloudformation deploy \
800 | --template-file aws_serverless_application_model.out.yaml \
801 | --capabilities CAPABILITY_IAM \
802 | --stack-name $CLOUDFORMATION_STACK_NAME \
803 | --region us-east-1
804 | Verify
809 |Find out the URL of the API Gateway endpoint via Cloudformation Template outputs:
811 |aws cloudformation describe-stacks \
815 | --stack-name $CLOUDFORMATION_STACK_NAME \
816 | --query Stacks[0].Outputs[0]
817 | This will give you a URL like:
821 |------------------------------------------------------------------------------
825 | | DescribeStacks |
826 | +-----------+----------------------------------------------------------------+
827 | | OutputKey | OutputValue |
828 | +-----------+----------------------------------------------------------------+
829 | | URL | https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod |
830 | +-----------+----------------------------------------------------------------+
831 | Now try to issue a curl request against it:
835 |$ curl https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod
839 | Hello, World!
840 | Generate Goa API backend
846 |Create design.go
848 |package design
851 |
852 | import (
853 | . "github.com/goadesign/goa/design"
854 | . "github.com/goadesign/goa/design/apidsl"
855 | )
856 |
857 | var _ = API("HelloServerlessGoa", func() {
858 | Title("Goa Server API Example")
859 | Description("Goa API powered by AWS Lambda and API Gateway")
860 | Scheme("http")
861 | Host("localhost:8080")
862 | })
863 |
864 | var _ = Resource("hello", func() {
865 | BasePath("/hello")
866 | DefaultMedia(HelloMedia)
867 |
868 | Action("show", func() {
869 | Description("Say Hello")
870 | Routing(GET("/:whatToSay"))
871 | Params(func() {
872 | Param("whatToSay", String, "What To Say Hello To")
873 | })
874 | Response(OK)
875 | Response(NotFound)
876 | })
877 | })
878 |
879 | var HelloMedia = MediaType("application/vnd.hello+json", func() {
880 | Description("Hello World")
881 | Attributes(func() {
882 | Attribute("hello", String, "What was said")
883 | Required("hello")
884 | })
885 | View("default", func() {
886 | Attribute("hello")
887 | })
888 | })
889 | Generate goa code
894 |Generate the controller, which we will customize:
896 |goagen controller --force --pkg controller -d github.com/tleyden/serverless-forms/design -o ./controllers
900 | and the remaining goa generated code, which we won’t touch.
904 |goagen app -d github.com/tleyden/serverless-forms/design -o ./goa-generated
908 | goagen client -d github.com/tleyden/serverless-forms/design -o ./goa-generated
909 | goagen swagger -d github.com/tleyden/serverless-forms/design -o ./goa-generated
910 | Generate the main
scaffolding:
goagen main -d github.com/tleyden/serverless-forms/design
918 | and remove the hello.go
which we don’t need, since it’s already in the controllers
directory
rm hello.go
926 | Goa fixups
931 |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.
-
936 |
-
937 |
Open
938 |main.go
and939 |951 |-
940 |
-
941 |
Change the
942 |app
package import togoa-generated/app
943 | -
944 |
Add this package import:
945 |controller "github.com/tleyden/serverless-forms/controllers"
946 | -
947 |
Change
948 |c := NewHelloController(service)
→c := controller.NewHelloController(service)
949 |
952 | -
941 |
-
953 |
Open
954 |controllers/hello.go
and change theapp
package import togoa-generated/app
955 |
Run goa standalone server
960 |go run main.go
963 | and you should see output:
967 |2017/06/04 12:32:00 [INFO] mount ctrl=Hello action=Show route=GET /hello/:whatToSay
971 | 2017/06/04 12:32:00 [INFO] listen transport=http addr=:8080
972 | and if you curl:
976 |$ curl localhost:8080/hello/foo
980 | {"hello":""}
981 | Customize controller behavior
986 |Open controllers/hello.go
and look for this line:
res := &app.Hello{}
992 | and add a new line, so it’s now:
996 |res := &app.Hello{}
1000 | res.Hello = ctx.WhatToSay
1001 | Now return the goa api server via go run main.go
, and retry that curl request:
$ curl localhost:8080/hello/world
1009 | {"hello":"world"}
1010 | and it now echos the parameter passed along the request path.
1014 |Deploy Goa API backend to Lambda
1019 |Merge the handler.go and main.go files
1021 |At this point there are two files that need to have their functionality merged:
1023 |-
1026 |
-
1027 |
1028 |handler.go
— this contains the Lambda / API Gateway stub code that was previously pushed up to AWS in a previous step
1029 | -
1030 |
1031 |main.go
— this contains the goa REST API server
1032 |
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
1040 |
1041 | package main
1042 |
1043 | import (
1044 | "net/http"
1045 |
1046 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
1047 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
1048 | "github.com/goadesign/goa"
1049 | "github.com/goadesign/goa/middleware"
1050 | controller "github.com/tleyden/serverless-forms/controllers"
1051 | "github.com/tleyden/serverless-forms/goa-generated/app"
1052 | )
1053 |
1054 | func createGoaService() *goa.Service {
1055 |
1056 | // Create service
1057 | service := goa.New("HelloServerlessGoa")
1058 |
1059 | // Mount middleware
1060 | service.Use(middleware.RequestID())
1061 | service.Use(middleware.LogRequest(true))
1062 | service.Use(middleware.ErrorHandler(service, true))
1063 | service.Use(middleware.Recover())
1064 |
1065 | // Mount "hello" controller
1066 | c := controller.NewHelloController(service)
1067 | app.MountHelloController(service, c)
1068 |
1069 | return service
1070 | }
1071 |
1072 | func main() {
1073 |
1074 | service := createGoaService()
1075 |
1076 | // Start service
1077 | if err := service.ListenAndServe(":8080"); err != nil {
1078 | service.LogError("startup", "err", err)
1079 | }
1080 |
1081 | }
1082 |
1083 | // Handle is the exported handler called by AWS Lambda.
1084 | var Handle apigatewayproxy.Handler
1085 |
1086 | func init() {
1087 |
1088 | ln := net.Listen()
1089 |
1090 | // Amazon API Gateway Binary support out of the box.
1091 | Handle = apigatewayproxy.New(ln, nil).Handle
1092 |
1093 | service := createGoaService()
1094 |
1095 | // Any Go framework complying with the Go http.Handler interface can be used.
1096 | // This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, etc.
1097 | go http.Serve(ln, service.Mux)
1098 |
1099 | }
1100 | Deploy to AWS Lambda
1106 |Re-run the same steps previously mentioned in Deploy aws-lambda-go-shim behind API Gateway
1108 |$ make
1112 | $ aws cloudformation package \
1113 | --template-file aws_serverless_application_model.yaml \
1114 | --output-template-file aws_serverless_application_model.out.yaml \
1115 | --s3-bucket $S3_BUCKET
1116 | $ aws cloudformation deploy \
1117 | --template-file aws_serverless_application_model.out.yaml \
1118 | --capabilities CAPABILITY_IAM \
1119 | --stack-name $CLOUDFORMATION_STACK_NAME \
1120 | --region us-east-1
1121 | $ aws cloudformation describe-stacks \
1122 | --stack-name $CLOUDFORMATION_STACK_NAME \
1123 | --query Stacks[0].Outputs[0]
1124 | Verify
1129 |$ curl https://7phv3wewuk.execute-api.us-east-1.amazonaws.com/Prod/hello/serverless-goa-world
1132 | {"hello":"serverless-goa-world"}
1133 |