AWS Serverless: Padrões Serverless Implementados (Parte 2 de 2)
Eduardo Rabelo
Posted on February 9, 2020
Continuaremos com a implementação de alguns padrões serverless descritos pelo Jeremy Daly.
- Confira a primeira parte em: AWS Serverless: Padrões Serverless Implementados (Parte 1 de 2)
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: 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 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 oprincipalId
do usuário e, comoStatement
, uma Política do IAM válida que permita o acesso ao endpoint. O arn do endpoint vem no evento na propriedademethodArn
. Você pode adicionar propriedades no objetocontext
(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: 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
.
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: 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). 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!!
Posted on February 9, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.