O lado Ops do Serverless (JS) – Melhores práticas para AWS

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:

Forte abraço, e até a próxima.