Serverless Framework: Dividindo sua API na AWS
Tags: ptbr, aws, serverless, cloud
Published: 2020-04-13 02:31:16

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

```bash
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](https://serverless.com/blog/serverless-workaround-cloudformation-200-resource-limit/). 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:

```bash
/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`:

```yaml
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`):

```yaml
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:

```yaml
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:

```yaml
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:

```yaml
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](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-authorizer.html))

## 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:

```yaml
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:

```yaml
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`:

```yaml
# ====================
# 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"
```

2. Exporte-os como _Outputs_ (veremos como fazer isso em uma seção mais a frente)
3. Diga ao Serverless Framework para usar esses recursos de API nas stacks filhas:

```yaml
# ====================
# 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").

```yaml
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.

```yaml
# ====================
# 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:

```yaml
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](https://www.gorillastack.com/news/splitting-your-serverless-framework-api-on-aws/), escrito originalmente por [Chris Armstrong](https://twitter.com/ckarmstrong).