Serverless Framework: Dividindo sua API na AWS

Image for post
Image for post

O Serverless Framework realmente acelerou o desenvolvimento de APIs para novos aplicativos, principalmente para backend de aplicativos Mobile ou Web, expondo os sistemas existentes por meio de uma API para integração. Quando combinada com o modelo do AWS Lambda + API Gateway para desenvolvimento de APIs, facilitando a proposição de custos por meio do modelo “pague apenas pelo tempo em que seu código for executado”.

Os desenvolvedores podem colocar algo rodando em apenas alguns dias, mesmo em casos mais ambiciosos, para validar a portabilidade de um aplicativo monolítico para o Lambda. Isso é verdade para aplicativos pequenos e a maioria dos exemplos demonstra bem as APIs menores. No entanto, aplicativos mais sérios podem ter dezenas ou centenas de APIs, o que apresenta seus próprios desafios. Um pouco de planejamento pode evitar dores de cabeça graves no caminho.

O temido limite de 200 recursos do CloudFormation

Error --------------------------------------------------The CloudFormation template is invalid: Template format error: Number of resources, 237, is greater than maximum allowed, 200

Esse problema será familiar para qualquer pessoa que tenha desenvolvido grandes aplicativos na AWS usando sua linguagem de modelos nativa, CloudFormation. O Serverless Framework usa o CloudFormation por baixo dos panos e não oferece uma solução fácil para esse problema.

Cada endpoint da API pode gerar algo entre 5 a 8 recursos do CloudFormation, o que praticamente limita o número de APIs em uma única stack serverless para algo entre 24 e 39. A solução geral para esse problema é dividir suas APIs em várias stacks. Como você verá, é recomendável planejar isso desde o início do seu projeto.

Então o que eu preciso fazer?

Sem usar nenhum plugin especial, o Serverless Framework fornece alguns helpers para tornar essa tarefa pelo menos possível e sua documentação sugere alguns plugins que podem ser úteis. Eles dependem de stacks aninhadas, que são mais avançadas e com complicações próprias! Vou descrever um método que usa várias stacks relacionadas, mas não stacks aninhadas.

Abordaremos isso criando uma stack base com nossos recursos compartilhados e estruturando-a para que possamos criar stacks dependentes (filhas) que contêm nossas implementações de API.

Então, precisamos:

Estou usando um aplicativo TODO com exemplo, estruturado da seguinte maneira:

/serverless.yaml   # Stack base
/api # Stacks dependentes
/api/users # Sub-stack 1 - Users
/api/users/serverless.yaml
/api/posts # Sub-stack 2 - Posts
/api/posts/serverless.yaml

Planejando os caminhos da API

Uma das coisas mais importantes a considerar é como os caminhos HTTP dos endpoints serão estruturados em sua API. Por exemplo, digamos que estamos planejando uma API como a seguinte estrutura:

Nosso primeiro plano de divisão seria colocar todas as APIs /users* na stack Users e as APIS /posts* na stack Posts. Logicamente, no entanto, a API /users/me/posts apresenta um problema porque realmente pertence à stack /posts, apesar de começar com /users.

Se formos estruturá-lo assim, isso também nos apresenta um problema prático. O API Gateway requer um recurso para cada elemento do caminho, ou seja, /users/{userId} requer dois recursos users e {userId}, e, em seguida, um recurso para o método, por exemplo POST. Eles são hierárquicos, pois cada um declara um recurso pai.

Serverless Framework geraria algo como o seguinte para nossa declaração PUT /users/me:

Resources:
# ====================
# /users (Referência a API Gateway como recurso pai)
# ====================
ApiGatewayResourceUsers:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: { Ref: "ApiGatewayRestApi" }
ParentId: { Fn::GetAtt: "ApiGatewayRestApi.RootResourceId" }
PathPart: users
# ====================
# /me (Referência /users como recurso pai)
# ====================
ApiGatewayResourceUsersMe:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: { Ref: "ApiGatewayRestApi" }
ParentId: { Ref: "ApiGatewayResourceUsers" }
PathPart: "me"
# ====================
# /users/me PUT (Referência /me como recurso pai)
# ====================
ApiGatewayResourceUsersMePosts:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: { Ref: "ApiGatewayRestApi" }
ResourceId: { Ref: "ApiGatewayResourceUsersMe" }
HttpMethod: PUT
...

Quando declaramos nossos outros endpoints que compartilham as mesmas partes do caminho (por exemplo, GET /users ou GET /users/me/posts), eles referenciam os mesmos recursos:

Se os endpoints forem declarados em stacks diferentes, e se nós não fizermos referência ao mesmo pai, o Serverless Framework tentará gerar os recursos do caminho duas vezes (nesse caso ApiGatewayResourceUsers) e a segunda tentativa falhará com um conflito. Somente os recursos AWS::ApiGateway::Method serão únicos.

Isso significa que precisamos identificar as partes do caminho que são compartilhadas, declará-las e exportá-las em nossa stack base e instruir Serverless Framework para usar as partes compartilhadas em nossas stacks filhas.

Stack base

Nossa stack base conterá todos os nossos recursos comuns. Eles serão exportados usando o CloudFormation Outputs. Você notará que preferimos declarar muitos dos recursos diretamente usando a sintaxe do CloudFormation — isso ocorre porque a maneira com que Serverless Framework os gera, dificulta o compartilhamento entre as stacks.

IAM Role

Embora você possa criar uma função do IAM por stack (onde faz sentido) ou mesmo por Lambda, uma função compartilhada é mais fácil e gera apenas um recurso.

Podemos usar a função serverless gerada e adicionar nossas próprias instruções com iamRoleStatements(e.g. declarando no provider):

name: aws
...
iamRoleStatements:
# ====================
# As declarações seguintes precisam ser adicionadas mesmo se você
# não estiver criando nenhum evento HTTP na sua stack base
# ====================
-
Effect: "Allow"
Action: \[ logs:CreateLogStream \]
Resource:
Fn::Join:
- ""
- \["arn:aws:logs:", {Ref: "AWS::Region"}, {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-\*:\*"\]
-
Effect: Allow
Action: \[ logs:PutLogEvents \]
Resource:
Fn::Join:
- ""
- \["arn:aws:logs:", {Ref: "AWS::Region"}, {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-\*:\*:\*"\]
# ====================
# Para outras Lambdas
# ====================
-
Effect: Allow
Action: \["sqs:SendMessage", "sqs:SendMessageBatch"\]
Resource:
Fn::GetAtt: MySQSQueue.Arn
-
Effect: Allow
Action: \["sns:Publish"\]
Resource:
Fn::GetAtt: MySNSTopic.Arn

Em seguida, exportamos o ARN do recurso IamRoleLambdaExecution gerado na seção Outpus (exemplo abaixo).

Nota: a IAM Role não é gerada automaticamente, a menos que você especifique pelo menos uma Lambda, como o autorizador. Nesse caso, você pode declarar a IAM Role diretamente, por exemplo:

resources:
Resources:
IAMRoleLambdaExecution:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service: \[ "lambda.amazonaws.com" \]
Action: \[ "sts:AssumeRole" \]
Path: "/"
RoleName: { "Fn::Join": \[ "-", \[ "postsapi", "dev", "ap-southeast-2", "lambdaRole" \] \] }
Policies:
- PolicyName: "${opt:stage}-postsapi-lambda"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action: \[ logs:CreateLogStream \]
Resource:
Fn::Join:
- ""
- \["arn:aws:logs:", {Ref: "AWS::Region"}, {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-\*:\*"\]
-
Effect: Allow
Action: \[ logs:PutLogEvents \]
Resource:
Fn::Join:
- ""
- \["arn:aws:logs:", {Ref: "AWS::Region"}, {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-\*:\*:\*"\]

Autorizador

Se você estiver implementando um autorizador, você deve declarar sua Lambda, por exemplo:

functions:
...
generalAuthorizer:
handler: authoriser.handler

e criar um recurso API Gateway Authorizer e uma permissão lambda associada (para que o API Gateway possa invocá-la) usando a sintaxe CloudFormation:

resources:
Resources:
...
# ====================
# Authorizer
# ====================
ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
AuthorizerResultTtlInSeconds: 60
AuthorizerUri:
Fn::Join:
- ''
-
- 'arn:aws:apigateway:'
- Ref: "AWS::Region"
- ':lambda:path/2015-03-31/functions/'
- Fn::GetAtt: "GeneralAuthorizerLambdaFunction.Arn"
- "/invocations"
IdentitySource: method.request.header.Authorization
IdentityValidationExpression: "Bearer .+"
Name: api-${opt:stage}-authorizer
RestApiId: { Ref: ApiGatewayRestApi }
Type: TOKEN
ApiGatewayAuthorizerPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: GeneralAuthorizerLambdaFunction.Arn
Action: lambda:InvokeFunction
Principal:
Fn::Join: \["",\["apigateway.", { Ref: "AWS::URLSuffix"}\]\]

Observe que você precisa:

API Gateway

A menos que você declare uma função com um evento http, o Serverless Framework não gerará um recurso CloudFormation RestApi. Por esse motivo, você tem duas opções:

  1. Crie um recurso dummy com um evento http (criando efetivamente uma API “morta”/sem nada). Isso gerará um recurso AWS::ApiGateway::RestApi com o nome lógico ApiGatewayRestApi e a IAM Role associada.
  2. Declare o seu. Isso é direto o suficiente:
resources:
Resources:
...
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: postsapi
Description: Posts API Gateway

Nas stacks filhas, podemos dizer ao Serverless Framework para usar nosso API Gateway compartilhado como seu recurso raiz ao invés de criar um novo:

provider:
name: aws
...
apiGateway:
# ====================
# Iremos definir os Exports para esses recursos mais tarde
# ====================
restApiId:
Fn::ImportValue: postsapi-${opt:stage}-RestApiId
restApiRootResourceId:
Fn::ImportValue: postsapi-${opt:stage}-RootResourceId

Criando e exportando os API Paths / caminhos da API

Felizmente, isso não é muito difícil:

  1. Na stack base, declare os recursos da parte do caminho na seção Resources que será compartilhada entre as stacks. No nosso caso, estas são as partes /users e /me:
# ====================
# Stack Base
# ====================
resources:
Resources:
...
ApiGatewayResourceUsers:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: { Ref: "ApiGatewayRestApi" }
ParentId: { Fn::GetAtt: "ApiGatewayRestApi.RootResourceId" }
PathPart: users
ApiGatewayResourceUsersMe:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: { Ref: "ApiGatewayRestApi" }
ParentId: { Ref: "ApiGatewayResourceUsers" }
PathPart: "me"
  1. Exporte-os como Outputs (veremos como fazer isso em uma seção mais a frente)
  2. Diga ao Serverless Framework para usar esses recursos de API nas stacks filhas:
# ====================
# Stacks Filhas
# ====================
provider:
name: aws
...
apiGateway:
...
restApiResources:
/users:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayResourceUsers
/users/me:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayResourceUsersMe

Exportando nossos recursos compartilhados

Os recursos compartilhados podem ser exportados na seção Outputs do CloudFormation. Eu escolhi colocar o nome do estágio (usando a propriedade ${opt:stage}) nos nomes de exportação para evitar conflitos se as mesmas stacks forem implementadas duas vezes na mesma conta e região (por exemplo, para implementar uma versão "uat" e uma para cada "branch").

resources:
...
Outputs:
# ====================
# O ID do recurso RestApi (ex: ei829oe)
# ====================
RestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: postsapi-${opt:stage}-RestApiId
# ====================
# O recurso raiz do RestAPI (caminho '/' é implícito)
# ====================
RootResourceId:
Value:
Fn::GetAtt: ApiGatewayRestApi.RootResourceId
Export:
Name: postsapi-${opt:stage}-RootResourceId
# ====================
# O IAM Role de execução da Lambda
# ====================
IamRoleLambdaExecution:
Value:
Fn::GetAtt: IamRoleLambdaExecution.Arn
Export:
Name: postsapi-${opt:stage}-IamRoleLambdaExecution
# ====================
# O Autorizador (caso você esteja usando API Gateway Custom Authorizer)
# ====================
ApiGatewayAuthorizerId:
Value:
Ref: ApiGatewayAuthorizer
Export:
Name: postsapi-${opt:stage}-ApiGatewayAuthorizerId
# ====================
# Caminhos de Recursos
# ====================
ApiGatewayResourceUsers:
Value:
Ref: ApiGatewayResourceUsers
Export:
Name: postsapi-${opt:stage}-ApiGatewayResourceUsers
ApiGatewayResourceUsersMe:
Value:
Ref: ApiGatewayResourceUsersMe
Export:
Name: postsapi-${opt:stage}-ApiGatewayResourceUsersMe

Stacks Filhas

Quando tivermos uma stack de base sólida, podemos começar a definir nossas stacks filhas. As partes principais são referenciar os recursos da API Gateway, como o Raiz, os Caminhos e a IAM Role na seção provider e, em seguida, referenciar o Authorizer em cada evento http lambda.

Dei um exemplo abreviado abaixo dos valores globais e de algumas funções.

# ====================
# Stack Filha - /api/users/serverless.yaml
# ====================
name: postapi-users
provider:
name: aws
runtime: nodejs8.10
memorySize: 1024MB
timeout: 10
role:
Fn::ImportValue: postsapi-${opt:stage}-IamRoleLambdaExecution
apiGateway:
restApiId:
Fn::ImportValue: postsapi-${opt:stage}-RestApiId
restApiRootResourceId:
Fn::ImportValue: postsapi-${opt:stage}-RootResourceId
restApiResources:
users:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayResourceUsers
users/me:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayResourceUsersMe
functions:
usersGet:
handler: usersGet.handler
events:
- http:
method: GET
# ====================
# Você ainda providência o caminho completo, e o Serverless Framework
# irá descobrir baseado no `provider.apiGateway.restApiResources`
# se deve gerar ou referenciar os recursos
# ====================
path: /users
integration: lambda-proxy
authorizer:
type: CUSTOM
authorizerId:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayAuthorizerId
usersMe:
handler: usersMeGet.handler
events:
- http:
method: GET
# ====================
# Você ainda providência o caminho completo, e o Serverless Framework
# irá descobrir baseado no `provider.apiGateway.restApiResources`
# se deve gerar ou referenciar os recursos
# ====================
path: /users/me
integration: lambda-proxy
authorizer:
type: CUSTOM
authorizerId:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayAuthorizerId

Você pode simplificar um pouco mais o exemplo acima, declarando a seção do autorizador como uma variável personalizada e fazendo referência a ela, por exemplo:

custom:
authorizer:
type: CUSTOM
authorizerId:
Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayAuthorizerId
# ====================
# No evento da Lambda
# ====================
events:
- http:
method: GET
...
authorizer: ${self:custom.authorizer}

Exemplo Completo

Todo o código acima, incluindo funções que utilizam o DynamoDB, podem ser encontrados em:

https://github.com/GorillaStack/splitstack-postsapi.git

As instruções para executá-lo podem ser encontradas no arquivo README.md do projeto.

1 — As stacks aninhadas funcionam implantando uma stack pai que contém parâmetros passados ​​para as stacks filhas. A principal dificuldade na implantação de stacks aninhadas é quando algo dá errado — o CloudFormation reverterá todas as alterações em um conjunto de alterações específico, o que é bastante demorado durante o teste de desenvolvimento (e especialmente se você tiver muitas stacks antes do erro que tiver alterações )

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store