Go Serverless: construindo uma API usando Golang e AWS Lambda
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!
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
).
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
).
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
.
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.
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.