goa-lambda-api

module
v0.0.0-...-001d619 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2017 License: Apache-2.0

README

[%hardbreaks]

= 🎩 Deploy Goa API backend on AWS Lambda
:toc: left
:toclevels: 3

link:https://github.com/goadesign/goa[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 link:https://github.com/eawsy/aws-lambda-go-shim[eawsy/aws-lambda-go-shim] and link:https://github.com/eawsy/aws-lambda-go-net[aws-lambda-go-net].  

This guide walks you through the entire process.

== Installation

=== Deploy aws-lambda-go-shim 

NOTE: You might want to check the link:https://github.com/eawsy/aws-lambda-go-shim[latest instructions], in case these are out of date.

==== Create a project directory

```
mkdir serverless-forms; cd serverless-forms
```

Replace `serverless-forms` with your own project name.

==== Get dependencies

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
```

==== Add lambda function handler

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)

==== Build handler.zip

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
```

==== Create AWS Lambda IAM Role

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
```

==== Deploy to AWS Lambda

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
```

==== Verify

1. In the AWS Web Admin, go to the Lambda section 
2. Choose the `preview-go` lambda function
3. Under **Actions**, select **Test Function**
4. Hit the **Save and Test** button
5. Under "The area below shows the result returned by your function execution.", you should see "Hello World!" -- this means it worked!

=== Deploy aws-lambda-go-shim behind API Gateway

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 link:https://github.com/eawsy/aws-lambda-go-net[eawsy/aws-lambda-go-net] exposes a REST API endpoint.

NOTE: The latest version of these docs is available on the link:https://github.com/eawsy/aws-lambda-go-net[eawsy/aws-lambda-go-net]

==== Get dependencies

```
go get -u -d github.com/eawsy/aws-lambda-go-net/...
``` 

==== Update handler.go

```
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!"))
}
```

==== Rebuild handler.zip

```
make
```

==== Create SAML (AWS Serverless Application Model) file

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 an S3 bucket

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.  

==== Deploy to AWS Lambda

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
```

==== Verify 

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!
```

=== Generate Goa API backend

==== Create design.go

```
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 goa code  

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
```

==== Goa fixups

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 to `goa-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 the `app` package import to `goa-generated/app`

==== Run goa standalone server 

```
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":""}
```

==== Customize controller behavior

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.

=== Deploy Goa API backend to Lambda  

==== Merge the handler.go and main.go files

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)

}

```

=== Deploy to AWS Lambda

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]
```

=== Verify 

```
$ curl https://7phv3wewuk.execute-api.us-east-1.amazonaws.com/Prod/hello/serverless-goa-world
{"hello":"serverless-goa-world"}
```

Directories

Path Synopsis
app
tool
cli

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL