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:
- planejar os caminhos de nossa API
- declarar nossa stack base
- implementar recursos compartilhados, como autorizadores, funções do IAM e a API REST na stack base
- declarar manualmente os caminhos da API base
- exportar todos os recursos compartilhados usando o CloudFormation Exports
- montar um modelo para as stacks filhas
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:
-
/users
POST - Criar um usuário -
/users/me
GET - Obtenha o usuário atual -
/users/me
PUT - Atualiza as configurações atuais do usuário -
/users/me/posts
GET - Lista todas as postagens do usuário atual -
/posts
GET - Liste ou pesquise as postagens do sistema -
/posts
POST - Crie uma postagem -
/posts/{postId}
GET - Obtenha uma postagem específica
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:
-
/users
GET referência aApiGatewayResourceUsers
-
/users/me/posts
GET referência aApiGatewayResourceUsersMe
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:
- substitua o nome da função que você declarou pelo nome CloudFormation gerado. Eles tem o formato
{FunctionName}LambdaFunction
(onde a primeira letra é maiúscula). No nosso caso, a função foi chamadageneralAuthorizer
, portanto o nome no CloudFormation seráGeneralAuthorizerLambdaFunction
- defina os outros parâmetros do autorizador, tais como
Type
,IdentitySource
etc (veja a documentação)
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:
- 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ógicoApiGatewayRestApi
e a IAM Role associada. - 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:
- 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"
- Exporte-os como Outputs (veremos como fazer isso em uma seção mais a frente)
- 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
- Splitting your Serverless Framework API on AWS, escrito originalmente por Chris Armstrong.
Top comments (0)