Go Serverless: construindo uma API usando Golang e AWS Lambda

Luiz Mai
Red Ventures Brasil - Tech
8 min readAug 7, 2018

O conceito de serverless computing não é tão recente quanto se imagina: em 2006, a plataforma Zimki já oferecia os serviços de Function as a Service (FaaS), mesmo que de forma primitiva. No entanto, a empresa não conseguiu se destacar no mercado e acabou não indo pra frente.

Em 2014, a Amazon Web Services (AWS) lançou o AWS Lambda, que se tornou o primeiro serviço capaz de executar códigos de forma totalmente abstrata. Somente em janeiro de 2018 passou a ser possível executar códigos Go de forma nativa nas Lambdas (antes, era necessário usar algo como, por exemplo, Apex), e foi nesse momento em que a comunidade foi à loucura, implementando as mais diversas aplicações usando este serviço.

Dentre estas aplicações, uma delas é o desenvolvimento de APIs combinando os serviços da AWS Lambda, API Gateway e DynamoDB, e será exatamente sobre isso que falaremos neste artigo. 😁

Primeiros passos

Antes de dar início à criação da API, recomenda-se que todos os serviços da AWS sejam criados e configurados corretamente.

Obviamente, primeiro você deve acessar o dashboard da AWS e realizar o login! 😉 A estrutura da aplicação contará com três peças:

  • API Gateway: administra todas as tarefas relacionadas ao recebimento e tratamento das requisições para a API.
  • DynamoDB: banco de dados não relacional que será utilizado para armazenar as informações consumidas pela API.
  • Lambda: código a ser executado a fim de verificar a requisição realizada, obter o dado necessário, processá-lo e retornar para a API.

Para começar, selecione o serviço Lambda, clique no botão Create a function, dê o nome que preferir (aqui, chamaremos de go-serverless-api) e escolha Go como runtime. Para este projeto, usaremos uma role default: escolha Create a new role from template, dê um nome a ela (escolhemos microservice-role) e escolha Simple Microservice permissions como o template a ser usado. Na próxima página, selecione o input Select a test event e escolha Configure test events. Clicando em Create new test event, crie um body vazio ({}) e nomeie-o como preferir (escolhemos Blank). PRONTO! Lambda criado!

Configurações utilizadas na criação da Lambda.

Agora, vamos criar nosso DynamoDB. Mais uma vez, acesse a página do serviço, clique em Create table, dê um nome para a tabela (usaremos go-serverless-api) e escolha a sua chave primária (neste artigo, utilizaremos simplesmente id).

Configurações utilizadas na criação da tabela do DynamoDB.

Por fim, vá ao serviço do API Gateway e clique em Get Started. Escolha New API e dê um nome para a sua API (mais uma vez, chamaremos de go-serverless-api).

Configurações utilizadas na criação do API Gateway.

Para este artigo, vamos gerar duas rotas: uma de listar usuários e outra para criar um usuário. Na tela de Resources, clique no recurso que já vem por padrão (/), selecione Actions e, em seguida, Create Resource. Isso nos permitirá criar uma nova rota para nossa API. Nomeie sua rota (chamaremos de /users) e em seguida, clique sobre o novo recurso criado (no caso, o /users), clique novamente em Actions mas dessa vez selecione Create Method. Selecione o método GET, escolha Lambda function como o tipo de integração e marque o checkbox Use Lambda Proxy integration. Lembre-se de inserir o nome e a região onde se encontra sua Lambda. Em seguida, repita o procedimento da criação do método, mas desta vez, utilize o método POST.

Criando um novo resource para o API Gateway.
Configurações utilizadas para o método GET /users.

Pronto! Agora já temos toda a nossa infraestrutura na AWS pronta para rodar nossa aplicação, então vamos partir para a parte mais legal: código!

Codando a aplicação

De forma superficial, o fluxo de nossa aplicação será:

  • Recebe a requisição do API Gateway e verifica qual rota foi chamada
  • Chama a função correspondente àquela rota
  • Realiza operações no banco de dados
  • Retorna o resultado para o API Gateway

Em Go, uma função Lambda sempre chamará a main, que deverá invocar o handler necessário. Ou seja:

func main() {
lambda.Start(handler)
}

Como nossa aplicação lida com duas rotas diferentes, usaremos um router no lugar do handler, de forma que ele possa decidir o que fazer baseado na rota da requisição. Com isso, nosso arquivo main.go deverá ficar semelhante a esse:

// main.go

package main

import (
"net/http"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)

// handleCreateUser

// handleGetUsers

func router(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

if req.Path == "/users" {
if req.HTTPMethod == "GET" {
return handleGetUsers(req)
}
if req.HTTPMethod == "POST" {
return handleCreateUser(req)
}
}

return events.APIGatewayProxyResponse{
StatusCode: http.StatusMethodNotAllowed,
Body: http.StatusText(http.StatusMethodNotAllowed),
}, nil
}

func main() {
lambda.Start(router)
}

Basicamente teremos nossa função main que irá chamar nosso router, que irá decidir qual das funções ele deverá chamar: handleGetUsers para listar os usuários ou handleCreateUser para criar um novo. Você deve estar se perguntando porque utilizamos esses APIGatewayProxyResponse e APIGatewayProxyRequest do pacote events, certo? Basicamente, estes dois eventos permitem receber as requisições do API Gateway e enviar a resposta de uma forma que o serviço consiga entender. Além disso, graças ao APIGatewayProxyRequest temos acesso a variáveis importantes como Path e HTTPMethod que ajudarão no processo de roteamento.

Antes de criar os handlers, vamos criar um novo arquivo que chamaremos de user.go em que criaremos a entidade de User que usaremos. A título de simplificação, utilizaremos apenas atributos do tipo string.

// user.go

package main

type User struct {
ID string `json:"id"`
Name string `json:"name"`
}

Para realizar as operações no banco de dados, utilizaremos um outro arquivo db.go onde faremos a conexão ao DynamoDB e criaremos as funções de inserir um novo usuário e a de listar todos eles. A conexão se torna extremamente simples ao usarmos a biblioteca disponibiliza pela AWS:

// db.go

package main

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
const AWS_REGION = "sa-east-1"
const TABLE_NAME = "go-serverless-api"

var db = dynamodb.New(session.New(), aws.NewConfig().WithRegion(AWS_REGION))

Caso você tenha criado seu banco de dados em uma outra região ou tenha nomeado-o de outra forma, lembre-se de alterar o valor das constantes! Agora já podemos iniciar nossas consultas em cima do DynamoDB. Para isso, utilizaremos duas operações:

  • Scan: lista todos os itens da tabela.
  • PutItem: insere um novo item na tabela.

Ambas seguem um mesmo padrão: primeiro criamos o input com as informações necessárias, enviamos esse input como parâmetro da nossa operação que nos retornará um resultado no qual daremos um Unmarshal (se necessário), obtendo a resposta que desejamos. Adicione estas funções ao seu arquivo db.go:

// GetUsers retrieves all the users from the DB
func GetUsers() ([]User, error) {

input := &dynamodb.ScanInput{
TableName: aws.String(tableName),
}
result, err := db.Scan(input)
if err != nil {
return []User{}, err
}
if len(result.Items) == 0 {
return []User{}, nil
}

var users[]User
err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &users)
if err != nil {
return []User{}, err
}

return users, nil
}

// CreateUser inserts a new User item to the table.
func CreateUser(user User) error {

// Generates a new random ID
uuid, err := uuid.NewV4()
if err != nil {
return err
}

// Creates the item that's going to be inserted
input := &dynamodb.PutItemInput{
TableName: aws.String(tableName),
Item: map[string]*dynamodb.AttributeValue{
"id": {
S: aws.String(fmt.Sprintf("%v", uuid)),
},
"name": {
S: aws.String(user.Name),
},
},
}

_, err = db.PutItem(input)
return err
}

Agora que nossa camada do banco de dados está completa, podemos finalmente criar nossos handlers no arquivo main.go. Vale ressaltar alguns que os handlers receberão como parâmetro um APIGatewayProxyRequest e deverão retornar (APIGatewayProxyResponse, error) a fim de garantir que o API Gateway consiga processar essas informações. Logo, adicione as seguintes funções ao arquivo main.go:

func handleGetUsers(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

users, err := GetUsers()
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: http.StatusText(http.StatusInternalServerError),
}, nil
}

js, err := json.Marshal(users)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: http.StatusText(http.StatusInternalServerError),
}, nil
}

return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: string(js),
}, nil
}

func handleCreateUser(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

var user User
err := json.Unmarshal([]byte(req.Body), &user)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: err.Error(),
}, nil
}

err = CreateUser(user)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: err.Error(),
}, nil
}

return events.APIGatewayProxyResponse{
StatusCode: http.StatusCreated,
Body: "Created",
}, nil
}

Pronto! Agora que nosso código está completo, podemos finalmente fazer o deploy da aplicação!

Deploy na AWS

Para finalmente publicar a API, são necessários alguns poucos passos:

  • Compile seu código usando:
# Compile the code in the main file
$ GOOS=linux go build -o main

# Put your executable into a zip file
$ zip main.zip main
  • Vá até sua Lambda no console da AWS e faça o upload do .zip que foi criado na seção Function code.
  • No campo Handler, escreva main, já que é o nome que demos para nosso pacote.
  • No topo da página, clique em Save.
Fazendo o upload do arquivo na Lambda.

Pronto! Agora, se quiser testar, vá até o API Gateway, selecione uma das rotas e clique no ícone Test. Para o GET /users não há a necessidade de nenhum parâmetro, mas para o POST /users você deve preencher o campo Request Body com algo do tipo:

{
"name": "Felipe Smith"
}

Execute o teste e você deve receber a mensagem Created indicando que tudo ocorreu conforme planejado. Se você executar o teste da listagem de usuários agora, você deverá ver o usuário recém-criado na lista!

Como tudo está funcionando, clique em Actions, selecione Deploy API, crie um novo stage (daremos o nome de go-serverless-api, como sempre) e pronto! Você receberá a URL da sua API que poderá utilizar para fazer suas requisições!

Caso queira consultar o resultado final da API, você pode acessar o repositório no GitHub que, apesar de estar um pouco diferente, conta com tudo isso que foi mostrado (e um pouco mais!).

Para conhecer mais sobre o nosso trabalho, acesse os sites da Azulis e do IQ.

--

--