AWS Serverless — Padrões Serverless Implementados (Parte 2 de 2)

Eduardo Rabelo
6 min readFeb 9, 2020

--

Continuaremos com a implementação de alguns padrões serverless descritos pelo Jeremy Daly.

Configuração Comum

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

yarn

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

yarn add serverless --dev

E crie um script para fazer o deploy do projeto:

"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.

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

service: Gatekeeperplugins:
- 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 de Alex DeBrie.

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

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.

A API Interna

Você pode ler a explicação aqui.

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

service: InternalAPIplugins:
- 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.

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.

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.
service: InternalHandoffplugins:
- 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). 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:

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:

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.

Finalizando

Neste artigo, implementamos mais três padrões serverless deste ótimo artigo de Jeremy Daly. Continuaremos com isso no artigo a seguir.

Espero que tenha te ajudado!!

--

--

No responses yet