DEV Community

Cover image for Serverless Framework: Dividindo sua API na AWS
Eduardo Rabelo
Eduardo Rabelo

Posted on

Serverless Framework: Dividindo sua API na AWS

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 a ApiGatewayResourceUsers
  • /users/me/posts GET referência a ApiGatewayResourceUsersMe

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 chamada generalAuthorizer, 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:

  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

Top comments (0)