AWS Serverless: Padrões Serverless Implementados (Parte 2 de 2)
Tags: ptbr, serverless, aws, webdev
Published: 2020-02-09 10:02:03

---
title: AWS Serverless: Padrões Serverless Implementados (Parte 2 de 2)
published: true
description: Implementando padrão Serverless baseados no artigo de Jeremy Daly
tags: ptbr, serverless, aws, webdev
cover_image: https://images.unsplash.com/photo-1550645612-83f5d594b671?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80
---

Continuaremos com a implementação de alguns padrões serverless [descritos pelo Jeremy Daly](https://dev.to/oieduardorabelo/aws-serverless-padroes-de-micro-servicos-serverless-1gb5).

- Confira a primeira parte em: [AWS Serverless: Padrões Serverless Implementados (Parte 1 de 2)](https://dev.to/oieduardorabelo/aws-serverless-padroes-serverless-implementados-parte-1-de-2-91o)

## Configuração Comum

Todos os projetos terão uma configuração comum, o que é bastante simples. Primeiro, inicialize um projeto NodeJS:

```bash
yarn
```

Em seguida, instale o Serverless Framework como uma dependência de desenvolvedor:

```bash
yarn add serverless --dev
```

E crie um script para fazer o deploy do projeto:

```json
"scripts": {
	"deploy": "serverless deploy --aws-profile serverless-local"
}
```

Supondo que você tenha um perfil chamado `serverless-local` nas suas credências da AWS CLI (dentro de `~/.aws/credentials`).

## O Porteiro

[Você pode ler a explicação aqui](https://dev.to/oieduardorabelo/aws-serverless-padroes-de-micro-servicos-serverless-1gb5#03-o-porteiro).

A grande diferença dos padrões anteriores é que precisamos de um autorizador Lambda personalizado. Vamos dar uma olhada no arquivo `serverless.yml`:

```yml
service: Gatekeeper

plugins:
  - serverless-pseudo-parameters
  - serverless-iam-roles-per-function

provider:
  name: aws
  runtime: nodejs10.x
  region: ${opt:region, self:custom.defaultRegion}
  logs:
    restApi: true

custom:
  defaultRegion: eu-west-1
  tableName: ${self:provider.region}-GatekeeperTable
  authorizerTableName: ${self:provider.region}-GatekeeperAuthorizerTable

functions:
  GetItem:
    handler: src/functions/getItem.handler
    events:
      - http:
          method: get
          path: item/{itemId}
          authorizer: 
            name: CustomAuthorizer
            resultTtlInSeconds: 0
            identitySource: method.request.header.Authorization
            type: token
    environment:
      tableName: ${self:custom.tableName}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:getItem
        Resource: !GetAtt GatekeeperTable.Arn
  PutItem:
    handler: src/functions/putItem.handler
    events:
      - http:
          method: post
          path: item
          authorizer: 
            name: CustomAuthorizer
            resultTtlInSeconds: 0
            identitySource: method.request.header.Authorization
            type: token
    environment:
      tableName: ${self:custom.tableName}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:putItem
        Resource: !GetAtt GatekeeperTable.Arn
  CustomAuthorizer:
    handler: src/functions/authorizer.handler
    environment:
      tableName: ${self:custom.authorizerTableName}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:getItem
        Resource: !GetAtt AuthorizationTable.Arn

resources:
  Resources:
    GatekeeperTable:
      Type: AWS::DynamoDB::Table
      Properties:
        KeySchema:
          - AttributeName: id
            KeyType: 'HASH'
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: 'N'
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:custom.tableName}
    AuthorizationTable:
      Type: AWS::DynamoDB::Table
      Properties:
        KeySchema:
          - AttributeName: id
            KeyType: 'HASH'
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: 'N'
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:custom.authorizerTableName}
```

Estamos criando uma nova tabela para a autorização, na qual você pode armazenar o que precisar para autorizar um usuário. O autorizador personalizado lambda é apenas outra função. A diferença nas outras funções é que agora estamos definindo o autorizador. Na propriedade `authorizer.name`, definimos o nome da função lambda que autorizará a solicitação, no `authorizer.identitySource`, definimos o cabeçalho que gostaríamos de usar da requisição e, na propriedade `authorizer.type`, queremos receber como tipo `token`. Se você quiser saber mais sobre autorizadores personalizados, [consulte este artigo](https://www.alexdebrie.com/posts/lambda-custom-authorizers/) de [Alex DeBrie](https://twitter.com/alexbdebrie).

Vamos dar uma olhada no código da função autorizador personalizado:

```tsx
const AWS = require("aws-sdk");
const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.tableName;

module.exports.handler = async (event, context) => {
  console.log(event);

  const id = event.authorizationToken;
  const req = {
    TableName: tableName,
    Key: {
        'id': parseInt(id)
      }
  };

  const dynamodbResp = await dynamodb.get(req).promise();

  console.log(dynamodbResp);

  if (!dynamodbResp.Item){
    // 401
    context.fail('Unauthorized');
   
    // 403
    // context.succeed({
    //   "policyDocument": {
    //     "Version": "2012-10-17",
    //     "Statement": [
    //       {
    //         "Action": "execute-api:Invoke",
    //         "Effect": "Deny",
    //         "Resource": [
    //           event.methodArn
    //         ]
    //       }
    //     ]
    //   }
    // })
  }
	context.succeed({
		"principalId": dynamodbResp.Item.name,
		"policyDocument": {
		    "Version": "2012-10-17",
		    "Statement": [
			    {
				    "Action": "execute-api:Invoke",
				    "Effect": "Allow",
				    "Resource": event.methodArn
			    }
		    ]
		},
		"context": {
			"org": "my-org",
			"role": "admin",
			"createdAt": "2019-11-11T12:15:42"
		}
	});
};
```

A lógica do autorizador não é importante aqui. O importante é que você receba o token (o valor do cabeçalho da autorização no nosso caso) no `event.authorizationToken`. A outra parte importante é o que devemos retornar em um autorizador personalizado. Existem três casos principais aqui:

- **401:** Você precisa chamar `context.fail('Unauthorized');` 
- **Sucesso:** Você precisa chamar `context.success` passando uma política. Nesta política, você precisa especificar o `principalId` do usuário e, como `Statement`, uma Política do IAM válida que permita o acesso ao *endpoint*. O arn do *endpoint* vem no evento na propriedade `methodArn`. Você pode adicionar propriedades no objeto `context` (para adicionar dados personalizados, etc) que serão passados para sua lambda interna.
- **403:** Você precisa chamar `contex.success` mas com um Política do IAM.

[Você pode verificar o código final aqui](https://github.com/vgaltes/ServerlessPatterns/tree/master/Gatekeeper).

## A API Interna

[Você pode ler a explicação aqui](https://dev.to/oieduardorabelo/aws-serverless-padroes-de-micro-servicos-serverless-1gb5#04-a-api-interna).

Esse padrão é muito mais simples, o que mudará é a maneira como o lambda é chamado. Portanto, o `serverless.yml` é bastante simples:

```yml
service: InternalAPI

plugins:
  - serverless-iam-roles-per-function

provider:
  name: aws
  runtime: nodejs10.x
  region: ${opt:region, self:custom.defaultRegion}

custom:
  defaultRegion: eu-west-1
  tableName: ${self:provider.region}-InternalAPITable

functions:
  GetItem:
    handler: src/functions/getItem.handler
    environment:
      tableName: ${self:custom.tableName}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:getItem
        Resource: !GetAtt InternalAPITable.Arn
  PutItem:
    handler: src/functions/putItem.handler
    environment:
      tableName: ${self:custom.tableName}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:putItem
        Resource: !GetAtt InternalAPITable.Arn

resources:
  Resources:
    InternalAPITable:
      Type: AWS::DynamoDB::Table
      Properties:
        KeySchema:
          - AttributeName: id
            KeyType: 'HASH'
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: 'N'
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:custom.tableName}
```

Observe que agora as funções Lambda não têm nenhum evento. Então, como podemos chamar essas lambdas? Vamos criar um script para chamar a função `PutItem`.

```tsx
const AWS = require('aws-sdk');

AWS.config.region = "eu-west-1";

const lambda = new AWS.Lambda();

const base64data = Buffer.from('{"AppName" : "InternalAPIApp"}').toString('base64');

var params = {
    ClientContext: base64data, 
    FunctionName: "InternalAPI-dev-PutItem", 
    InvocationType: " RequestResponse", 
    // Escolhemos "Tails" para incluir o log de execução na resposta.
    LogType: "Tail",
    Payload: '{"id": 1, "name": "test1"}'
};

lambda.invoke(params, function(err, data) {
    if (err) console.log(err, err.stack); // um erro ocorreu
    else     console.log(data);           // resposta com sucesso
});
```

Como você pode ver, precisamos usar o AWS SDK. Você pode instalá-lo usando o `yarn add aws-sdk --dev`. O importante aqui é que precisamos especificar o nome da função. O tipo de chamada agora é `RequestResponse` que significa que aguardamos uma resposta da lambda (veremos o outro tipo no próximo padrão).

Portanto, supondo que você nomeie esse arquivo como `callPutItems.js` e que tenha um perfil chamado `serverless-local` você pode usar esse script como `AWS_PROFILE=serverless-local node callPutItem.js`.

## A Entrega Interna

[Você pode ler a explicação aqui](https://dev.to/oieduardorabelo/aws-serverless-padroes-de-micro-servicos-serverless-1gb5#05-a-entrega-interna).

Esse padrão é bastante semelhante ao anterior, com algumas diferenças:

- Iremos chamar a lambda com um tipo de invocação chamado `Event`, invocando a lambda de modo assíncrono. 
- Vamos adicionar um DLQ (no nosso caso, um tópico SNS) para as mensagens com falha.

```yml
service: InternalHandoff

plugins:
  - serverless-iam-roles-per-function
  - serverless-pseudo-parameters

provider:
  name: aws
  runtime: nodejs10.x
  region: ${opt:region, self:custom.defaultRegion}
  stage: ${opt:stage, self:custom.defaultStage}

custom:
  defaultRegion: eu-west-1
  defaultStage: dev
  tableName: ${self:provider.stage}-InternalHandofffTable
  dlqTopicName: ${self:provider.stage}-DLQTopicName
  dlqTopicArn: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:${self:custom.dlqTopicName}

functions:
  GetItem:
    handler: src/functions/getItem.handler
    environment:
      tableName: ${self:custom.tableName}
    onError: ${self:custom.dlqTopicArn}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:getItem
        Resource: !GetAtt InternalHandofffTable.Arn
      - Effect: Allow
        Action: sns:Publish
        Resource: ${self:custom.dlqTopicArn}
  PutItem:
    handler: src/functions/putItem.handler
    environment:
      tableName: ${self:custom.tableName}
    onError: ${self:custom.dlqTopicArn}
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:putItem
        Resource: !GetAtt InternalHandofffTable.Arn
      - Effect: Allow
        Action: sns:Publish
        Resource: ${self:custom.dlqTopicArn}
  ReadErrors:
    handler: src/functions/readErrors.handler
    events:
      - sns: ${self:custom.dlqTopicName}

resources:
  Resources:
    InternalHandofffTable:
      Type: AWS::DynamoDB::Table
      Properties:
        KeySchema:
          - AttributeName: id
            KeyType: 'HASH'
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: 'N'
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:custom.tableName}
```

A principal diferença nas funções é que definimos a propriedade `onError`. Definimos essa propriedade apontando para um arn de um tópico SNS (por hora, não podemos usar SQS, [verifique o porquê aqui](https://serverless.com/framework/docs/providers/aws/guide/functions/#dlq-with-sqs)). Quando fazemos isso, precisamos adicionar uma permissão para poder escrever nesse tópico.

Por fim, estamos criando uma função que lê esse tópico. Quando fizermos isso, o Serverless Framework criará o tópico do SNS para nós. Se não especificarmos nenhuma função, precisaremos criar o tópico na seção de `resources:`.

Para testar a DLQ, estamos definindo uma condição no código de nossas funções Lambdas para gerar um erro se o nome do item for "erro". Vamos ver como invocar essas lambdas com nosso script anterior e ver o que mudou:

```tsx
const AWS = require('aws-sdk');

AWS.config.region = "eu-west-1";

const lambda = new AWS.Lambda();
const name = process.argv.slice(2)[0];

const base64data = Buffer.from('{"AppName" : "InternalAPIApp"}').toString('base64');

const params = {
    ClientContext: base64data, 
    FunctionName: "InternalHandoff-dev-PutItem", 
    InvocationType: "Event", 
    LogType: "Tail",
    Payload: `{"id": 1, "name": "${name}"}`
};

lambda.invoke(params, function(err, data) {
    if (err) console.log(err, err.stack); // com erro
    else     console.log(data);           // com sucesso
});
```

Como você pode ver, o tipo de chamada agora é `Event`. Fazendo isso, receberemos um **202** da função ao invés de um **200**.

Para chamar esse script para gerar um erro, você pode fazer:

```bash
AWS_PROFILE=serverless-local node callPutItem.js error
```

Use qualquer outro nome se não quiser gerar um erro.

Quando fizermos isso, a AWS tentará entregar a mensagem três vezes (aproximadamente uma vez por minuto). Se falharmos nas três vezes, a mensagem será enviada para o DLQ.

[Você pode verificar o código final aqui](https://github.com/vgaltes/ServerlessPatterns/tree/master/InternalHandoff).

## Finalizando

Neste artigo, implementamos mais três padrões serverless [deste ótimo artigo de Jeremy Daly](https://dev.to/oieduardorabelo/aws-serverless-padroes-de-micro-servicos-serverless-1gb5). Continuaremos com isso no artigo a seguir.

Espero que tenha te ajudado!!