Vamos olhar o lado Ops do framework Serverless JS e alguns itens de melhores práticas. Este é um “catado” de vários texto e um toque das minhas considerações.
Para começar com a parte Ops devemos falar o óbvio:
Na conta da AWS, NÃO roda só o seu projeto.
É fundamental enfatizar isso, pois a nomenclatura adequada pode facilitar ou se tornar uma fonte de problemas. Sempre pergunte se já existe um modelo de nomes. Afinal, estamos falando da parte operacional (Ops) e não do desenvolvimento (Dev).
Vamos criar o nosso cenário: Imagine que vamos fazer uma estrutura de Serverless para uma locadora de filmes (sim, um exemplo total anos 90. Kappa). Vamos lidar somente com a parte de filmes do “sistema” (Domínio filmes para o padrão DDD)
Como de costume, aqui está o projeto no GIT para acompanhamento: https://github.com/escovabit-tec-br/serverless-locadora-filmes
Service Name
Começamos, pelo nome do nosso Serverless. No nosso exemplo ele fica:
service: sls-locadora-filmes
onde:
- sls: é a abreviação de Serverless
- locadora: é o “projeto”
- filmes: é o domínio
Esse nome é importante, pois é utilizado por vários componentes da AWS, como o CloudFormation, o ApiGateway e o conteúdo do Bucket S3. Podemos ver isso nas imagens abaixo:
CloudFormation
ApiGateway
S3
Params: artifactId e groupId
Mas nem tudo no Serverless vai usa o service name
como referência. Vamos precisar saber qual é o nome do “projeto” ou qual é o nome do “domínio” da aplicação separadamente. Para isso, criamos a configuração de params
, usamos ele porque nos permite fazer o uso de um valor default
, permite a espacialização por stage
, e permite a atualização por linha de comando.
params: default: # Exemplos de artifactId: usuarios, critografica, trasnferencias. artifactId: filmes # Exemplos de groupId: ecommercer, backoffice, crm groupId: locadora
Os nomes artifactId e groupId são usados por herança do Java.
Abaixo temos o exemplo da especialização por stage
:
params: # Nome da Tablela do Dynamo dev: tableName: locadora.filmes # Nome da tabela de DEV prd: tableName: locadora.filmes # Nome da tabela de PRD
Deployment Bucket
O Serverless, por padrão, cria um bucket com base no nome do serviço, mas os buckets têm nomes ‘globais’ dentro da AWS, o que aumenta a probabilidade de conflito entre dois Serverless diferentes usando o mesmo nome de bucket. Seja por causa do stage
ou por causa da region
.
Para evitar isso, é recomendável criar um bucket para cada projeto e estágio, o que facilita a identificação do bucket correspondente. Com isso a configuração do Serverless fica:
provider: deploymentBucket: # Name of an existing bucket to use (default: created by serverless) name: sls-${param:groupId}-${sls:stage, 'dev'}-${opt:region, 'us-east-1'}-deploys
Na AWS, podemos ver os Bukets assim:
Como esses nomes são customizados, o Serverless não vai criar os Buckets sozinhos, você vai precisar criar eles previamente na AWS. Uma boa forma é criar essa estrutura por algum framework de IaC (Terraform por exemplo).
Service IAM Role
O Serverless precisa interagir com uma função (role) no IAM com as permissões básicas de acesso ao Lambda para a execução do seu projeto. Contudo, é a mesma questão que o Bucket, nomes “globais”. Por isso, é bom especializar o nome da role
no IAM.
provider: iam: role: # Member must have length less than or equal to 64 name: ${self:service}-${sls:stage, 'dev'}-${opt:region, 'us-east-1'}
Desta forma, você terá uma role
com o service name
, stage
e region
.
Este item é criado automaticamente pelo Serverless para você.
Function IAM Role
Outro item de boa pratica, é configurar uma role de IAM para cada function
do Serverless. Desta forma, dando a permissão necessária para cada tipo de função. Esse elemento, depende da instalação do plugin serverless-iam-roles-per-function
no projeto.
No nosso exemplo, configuramos que a função getAll
pode fazer scan
no banco de dados, e que a função getById
só pode fazer getItem
functions: getAll: handler: functions/getAll/handler.getAll provisionedConcurrency: 1 # Limit 1 instance reservedConcurrency: 1 # Limit 1 thread iamRoleStatementsName: ${self:service}-${sls:stage, 'dev'}-${opt:region, 'us-east-1'}-getAll iamRoleStatements: - Effect: "Allow" Action: - dynamodb:Scan Resource: "arn:aws:dynamodb:${opt:region}:*:table/${param:tableName}" getByID: handler: functions/getById/handler.getById provisionedConcurrency: 1 # Limit 1 instance reservedConcurrency: 1 # Limit 1 thread iamRoleStatementsName: ${self:service}-${sls:stage, 'dev'}-${opt:region, 'us-east-1'}-getById iamRoleStatements: - Effect: "Allow" Action: - dynamodb:GetItem Resource: "arn:aws:dynamodb:${opt:region}:*:table/${param:tableName}"
Desta forma, nossas funções IAM ficam assim:
Organizadas por: projeto-dominio-ambiente-região-função
Package Patterns
Na AWS nada é de graça, e uma coisa que acaba criando um custo difícil de se localizar é os S3. Por isso, é uma boa pratica deixar o pacote do Serverless o menor possível. Para se faze isso, deve se remove do pacote tudo aquilo que não for necessário:
package: patterns: - "!tmp/**" - "!.git/**" - "!*.md" - "!jenkins.yml" - "!node_modules/**"
Custom Domain
Por fim, precisamos configurar como o API Gateway da AWS exportará nossas funções e utilizará o endereço de DNS.
Uma boa prática é utilizar no nome, dividindo por projeto
, ambiente
e região
. Exemplo:
- https://locadora.us-east-1.escovabit.tec.br – Produção
- https://locadora.dev.us-east-1.escovabit.tec.br – Desenvolvimento
No API Gateway, precisamos criar os domínios, esta também é uma configuração que também precisa ser feita de forma prévia. Mas é bem simples.
Com o domínio criado, basta pegar o nome do API Gateway (que possui esse nome esquisito) e atribuí-lo ao endereço do registro de DNS.
Configurando uma entrada no DNS do tipo CNAME.
No Serverless, precisamos do plugin serverless-domain-manager
para ele entender essas configurações do API Gateway e fazer a configuração abaixo:
custom: domain: dev: locadora.dev.${opt:region}.escovabit.tec.br prd: locadora.${opt:region}.escovabit.tec.br customDomain: domainName: ${self:custom.domain.${sls:stage, 'dev'}} basePath: v1 stage: ${sls:stage, 'dev'} certificateName: ${self:custom.domain.${sls:stage, 'dev'}} endpointType: regional apiType: http
Não esquecer de fazer a configuração do CORS
:
provider: httpApi: cors: allowedOrigins: - "https://${self:custom.domain.${sls:stage, 'dev'}}" # cors domains
Lista dos textos de referencias:
- https://www.datree.io/resources/serverless-best-practices
- https://serverless.readme.io/docs/best-practices
- https://serverless.readme.io/docs/project-structure
- https://aws.plainenglish.io/7-serverless-applications-security-best-practices-d04be33c7752
- https://www.serverless.com/blog/webinar-serverless-for-teams
- https://www.bmc.com/blogs/serverless-best-practices/
- https://pauldjohnston.medium.com/serverless-best-practices-b3c97d551535
Forte abraço, e até a próxima.