DEV Community

Cover image for Serverless TypeScript com AWS SAM
Eduardo Rabelo
Eduardo Rabelo

Posted on

Serverless TypeScript com AWS SAM

Aprenda a escrever Lambdas para AWS Serverless Application Model (SAM) em puro TypeScript sem a necessidade de comprometer seu fluxo de trabalho de desenvolvimento. Veja como confiar nas camadas compartilhadas do SAM para empacotar suas dependências. Consulte nosso repositório exemplo no GitHub para que você possa construir e implantar AWS Lambdas com TypeScript em um ambiente de produção completo.

Funções Serverless (ou AWS Lambdas na linguagem AWS) são uma ótima escolha quando a carga do seu aplicativo tende a ser altamente irregular e você deseja evitar o provisionamento de servidores e configuração de todo o ambiente para realizar algumas operações por alguns dias ou semanas por ano.

Existem muitas ferramentas para simplificar o desenvolvimento de funções serverless: Serverless Framework suporta várias plataformas, o AWS Serverless Application Model (SAM) que vem direto da AWS, entre outros.

O AWS SAM é uma ótima escolha se você já confia no ecossistema AWS. O SAM facilita a implantação de Lambdas junto com toda a infraestrutura em nuvem necessária (API Gateways, bancos de dados, filas, logs, etc.) com a ajuda de templates do AWS CloudFormation, que são amplamente usados no ecosistema.

Embora o AWS Lambda suporte muitas linguagens de programação. Node.js continua sendo minha escolha número um pela riqueza de seu ecossistema npm, a abundância de documentação online e exemplos e tempos de inicialização estelares. No entanto, escrever JavaScript puro é indiscutivelmente menos agradável do que executar JavaScript puro. Felizmente, estamos em 2021 e podemos contar com o TypeScript para trazer a alegria de escrever JS.

Há apenas um problema: o AWS SAM não oferece suporte ao TypeScript automáticamente.

É uma grande desvantagem, mas não um motivo para desistir e jogar fora seus tipos e verificação de tempo de compilação pela janela. Vamos ver como podemos construir nós mesmos uma experiência agradável de SAM-TypeScript: do desenvolvimento local à implantação na nuvem. E não vamos recorrer a hacks amplamente recomendados. Vamos lá!

O Bingo do TypeScript

Em meu mundo imaginário perfeito, é assim que o suporte adequado do TypeScript deve ser:

  • Manter a experiência de desenvolvimento local praticamente inalterada: sem mover package.json para outros lugares ou alterar a estrutura do diretório antes do deploy.
  • Sem executar sam build em cada mudança no código do manipulador da função
  • Manter o código JS gerado o mais próximo possível da fonte TS, preservando o layout do arquivo (não empacote tudo em um único arquivo como o webpack faz).
  • Manter dependências em uma camada separada compartilhada entre Lambdas relacionadas. Tornando o deploy mais rápido, pois você só precisa atualizar o código da função e não suas dependências. Além disso, as funções Lambda têm um limite de tamanho que pode ser facilmente atingido com dependências pesadas; camadas compartilhadas nos permitem manter a coloração entre as linhas.
  • Mantendo o deploy o mais simples possível: sam builde sam deploy, sem nenhuma mágica CLI extra.

Resumindo, uma AWS Lambda com TypeScript e camadas compartilhadas deve se comportar da mesma maneira que um AWS Lambda recém-gerado em Node.js.

Vamos ver como podemos conseguir isso!

1. Mova dependências para camadas compartilhadas

Eu revi alguns manuais sobre "como mover dependências Node.js para camadas Lambda" ( 1 , 2 ), mas não segui nenhum deles, pois eles propõem mover package.json da raiz do projeto para uma nova pasta, obrigatória, chamada dependencies e, assim, interromper o desenvolvimento e os testes locais.

Então me deparei com o documento oficial de Construíndo Camadas da AWS e decidi substituir o processo de construção do AWS SAM com Node.js ( sam build copia o código automagicamente, instala pacotes e faz a limpeza, mas você não pode interferir em suas decisões) por um personalizado um, com base em um Makefile.

Primeiro, declaramos nossas próprias camadas e quaisquer camadas de terceiros dentro de template.yml:

Globals:
  Function:
    Layers:
      # Nossa camada que iremos criar
      - !Ref RuntimeDependenciesLayer
      # Ao mesmo tempo, podemos referenciar camadas de terceiros
      - !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:464622532012:layer:Datadog-Node14-x:48"

  RuntimeDependenciesLayer:
    Type: AWS::Serverless::LayerVersion
    Metadata:
      BuildMethod: makefile # Aqui está o truque!
    Properties:
      Description: Runtime dependencies for Lambdas
      ContentUri: ./
      CompatibleRuntimes:
        - nodejs14.x
      RetentionPolicy: Retain
Enter fullscreen mode Exit fullscreen mode

Esta seção Metadata é um ponto chave aqui. Nós o adicionamos não apenas à nossa camada, mas também aos nossos Lambdas:

Metadata:
  BuildMethod: makefile
Enter fullscreen mode Exit fullscreen mode

Podemos então escrever um simples Makefile que mantém apenas o código executável dentro de um Lamda e coloca todas as dependências e node_modules dentro de uma camada separada que pode ser compartilhada com outras lambdas.

.PHONY: build-ExampleLambda build-RuntimeDependenciesLayer

build-ExampleLambda:
    cp -r src "$(ARTIFACTS_DIR)/"

build-RuntimeDependenciesLayer:
    mkdir -p "$(ARTIFACTS_DIR)/nodejs"
    cp package.json package-lock.json "$(ARTIFACTS_DIR)/nodejs/"
    npm install --production --prefix "$(ARTIFACTS_DIR)/nodejs/"
    # Para evitar ter que fazer o build quando as mudanças não se relacionam com dependências
    rm "$(ARTIFACTS_DIR)/nodejs/package.json"
Enter fullscreen mode Exit fullscreen mode

Dica: se você estiver usando o Yarn, precisará alternar --cwd ao invés do npm--prefix

Agora há muito mais observabilidade no processo de construção do Lambda, sem mágica!

No entanto, uma compilação baseada em Makefile também tem suas desvantagens: é difícil depurar seu processo de compilação. Consulte aws / aws-sam-cli # 2006 para obter detalhes.

Veja este commit em nosso repositório examplo para mais detalhes.

2. Migrar para TypeScript

Agora, como temos um pipeline de construção personalizável, podemos finalmente adicionar a etapa de compilação do TypeScript.

Primeiro, vamos instalar o próprio TypeScript. Coloque os seguintes pacotes em seu package.json e defina alguns scripts para compilar seu TypeScript para desenvolvimento e produção:

"dependencies": {
  "source-map-support": "^0.5.19"
},
"devDependencies": {
  "@tsconfig/node14": "^1.0.0",
  "@types/aws-lambda": "^8.10.72",
  "@types/node": "^14.14.26",
  "typescript": "^4.1.5"
},
"scripts": {
    "build": "node_modules/typescript/bin/tsc",
    "watch": "node_modules/typescript/bin/tsc -w --preserveWatchOutput"
}
Enter fullscreen mode Exit fullscreen mode

Em segundo lugar, crie e configure o seu tsconfig.json:

{
  "extends": "@tsconfig/node14/tsconfig.json",
  "compilerOptions": {
    "sourceMap": true,
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*.ts", "src/**/*.js"]
}
Enter fullscreen mode Exit fullscreen mode

Como o nodejs14.x no AWS Lambda funciona (obviamente) na última versão LTS do Node.js, podemos usar "target": "es2020" e "lib": ["es2020"] para construir o código JS que será muito, muito semelhante ao código TypeScript de origem, mantendo todos os asyncs e awaits.

Agora você pode substituir seu build-ExampleLamda no Makefile pela definição que inclui as etapas de instalação e compilação:

# Makefile
build-ExampleLambda:
    npm install
    npm run build
    cp -r dist "$(ARTIFACTS_DIR)/"
Enter fullscreen mode Exit fullscreen mode

3. Usando "sam build" no desenvolvimento local

Os comandos como sam local invoke ou sam local start-api procuram primeiro a pasta .aws-sam/ e, se não houver nenhuma, eles procuram pelo manipulador da lambda na pasta atual.

É importante remover o diretório gerado automaticamente .aws-sam após cada deploy, para que o SAM possa ver suas alterações locais sem ser executar sam build constantemente.

Precisamos apenas garantir que o código TypeScript compilado esteja localizado no mesmo caminho de uma lambda deployada e localmente.

Por padrão, o código Node.js para uma lamda está localizado na pasta src/, mas agora contém nosso código TypeScript, portanto, precisamos colocar nosso código compilado em outro lugar. Vamos pegar emprestado uma convenção popular do pessoal do front-end e apresentar a pasta dist para o JavaScript final. Mude o seu template.yml para:

--- a/template.yml
+++ b/template.yml
@@ -29,7 +29,7 @@ Resources:
     Metadata:
       BuildMethod: makefile
     Properties:
-      Handler: src/handlers/get-all-items.getAllItemsHandler
+      Handler: dist/handlers/get-all-items.getAllItemsHandler
Enter fullscreen mode Exit fullscreen mode

Tudo que você precisa agora é iniciar seu compilador TypeScript no modo de observação, para que seu código compile magicamente em cada alteração (ou não, mas então você saberá imediatamente o porquê). Felizmente, já cuidamos disso em nosso package.json. Apenas não se esqueça de executar isso em seu terminal antes de começar a codificar (ou iniciar uma tarefa de observação em seu IDE favorito).

$ npm run watch
Enter fullscreen mode Exit fullscreen mode

Você não precisa mais executar localmente sam build, e comandos como sam local start-api poderão ver suas alterações imediatamente (porque eles apontam para o código transpilado, não para o código-fonte).

E se você tiver um inicializador Procfile como o Overmind (qualidade marciana, altamente recomendado!), Você pode configurar a inicialização do compilador SAM e TS em paralelo em Procfile:

# Procfile
sam: sam local start-api
tsc: npm run watch
Enter fullscreen mode Exit fullscreen mode

Em seguida, use-o para iniciar o compilador TypeScript no modo de observação e um API Gateway local como dois processos simultâneos:

$ overmind start
Enter fullscreen mode Exit fullscreen mode

E é isso!

Veja um resumo das mudanças que fizemos nesta etapa deste commit em nosso repositório de exemplo.

4. Escrevendo seu código

Também é uma boa ideia ativar o suporte a mapas de origem, portanto, podemos rastrear de pilha do TypeScript em caso de qualquer erro:

import "source-map-support/register";
Enter fullscreen mode Exit fullscreen mode

Também precisamos incluir os tipos específicos da AWS em nossos manipuladores:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
Enter fullscreen mode Exit fullscreen mode

E declare os manipuladores que os usam:

export const getAllItemsHandler = async (
    event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
    // ...mais código
}
Enter fullscreen mode Exit fullscreen mode

Veja o exemplo completo de um manipulador digitado corretamente neste exemplo.

5. Configurando testes

  1. Adicione Jest com suporte TypeScript ao seu package.json:
"devDependencies": {
  "@types/jest": "^26.0.20",
  "jest": "^26.6.3",
  "ts-jest": "^26.5.1",
},
Enter fullscreen mode Exit fullscreen mode
  1. Adicione a configuração relacionada ao TypeScript ao seu jest.config.js
module.exports = {
  preset: "ts-jest",
  modulePathIgnorePatterns: ["<rootDir>/.aws-sam"],
};
Enter fullscreen mode Exit fullscreen mode

E é isso! Reescreva seus testes em TS e execute-os com npm t, como você fez antes.

6. Depuração (Debug)

Com essa abordagem, a depuração não só é possível, mas também funciona imediatamente!

Você pode usar um depurador externo seguindo este manual da AWS: Depuração passo a passo de funções Node.js localmente .

  1. Execute sam local invoke com a opção --debug-port.
$ sam local invoke getAllItemsFunction --event events/event-get-all-items.json --debug-port 5858
Enter fullscreen mode Exit fullscreen mode

Isso aguardará a conexão de um depurador antes de iniciar a execução da função.

  1. Coloque um ponto de interrupção onde necessário (sim, direto no seu código TypeScript!)

  2. Inicie o depurador externo (no Visual Studio Code, você pode simplesmente pressionar F5).

E aqui está um exemplo de código VSCode .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to SAM CLI",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 5858,
      "localRoot": "${workspaceRoot}/",
      "remoteRoot": "/var/task",
      "protocol": "inspector",
      "stopOnEntry": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Bônus: Coloque apenas o código relevante em suas funções Lambdas

Embora seja conveniente ter muitas funções Lambda relacionadas juntas em um projeto SAM com configuração comum, dependências e código utilitário comum (reutilizado por algumas, mas não todas as funções), parece redundante fazer o deploy de todas as funções de uma vez quando você muda um pouco de código que é usado apenas por uma delas.

Quero que cada função use apenas os arquivos que são realmente usados ​​por esta função, sem importações estranhas. Esse é o meu maximalismo estético puro, já que não faz mal importar todos os arquivos em todas as funções: os arquivos de código-fonte são muito leves. Mas o TypeScript nos permite alcançar a pureza ! Quando o compilador TypeScript recebe um caminho para um único arquivo, ele apenas compila esse arquivo e suas dependências, nada mais.

Dado que cada função do Lambda tem um manipulador (uma única função de ponto de entrada em um único arquivo), podemos compilar apenas esse manipulador, e isso nos fornecerá apenas os arquivos necessários para executar um determinado Lambda.

Isso nos permite economizar nas linhas do código-fonte e (mais importante) fazer o deploy apenas das funções que usam o código alterado.

Porém, para fazer o TSC honrar nossa configuração tsconfig.json, precisamos de um pequeno hack (veja microsoft / TypeScript # 27379 (comentário) para mais detalhes).

Aí vem:

build-lambda-common:
    npm install
    rm -rf dist
    echo "{\"extends\": \"./tsconfig.json\", \"include\": [\"${HANDLER}\"] }" > tsconfig-only-handler.json
    npm run build -- --build tsconfig-only-handler.json
    cp -r dist "$(ARTIFACTS_DIR)/"

build-getAllItemsFunction:
    $(MAKE) HANDLER=src/handlers/get-all-items.ts build-lambda-common
build-getByIdFunction:
    $(MAKE) HANDLER=src/handlers/get-by-id.ts build-lambda-common
build-putItemFunction:
    $(MAKE) HANDLER=src/handlers/put-item.ts build-lambda-common

Enter fullscreen mode Exit fullscreen mode

E agora para a seguinte estrutura de arquivo do nosso projeto:

.
└── src
    ├── handlers
    │   ├── a.ts
    │   ├── b.ts
    │   └── c.ts
    └── utils
        ├── ab.ts
        └── bc.ts

Enter fullscreen mode Exit fullscreen mode

Obteremos as três funções lambda a seguir:

.aws-sam/build/FunctionA
└── dist
    ├── handlers
    │   ├── a.js
    │   └── a.js.map
    └── utils
        ├── ac.js
        └── ac.js.map

.aws-sam/build/FunctionB
└── dist
    ├── handlers
    │   ├── b.js
    │   └── b.js.map
    └── utils
        ├── ab.js
        ├── ab.js.map
        ├── bc.js
        └── bc.js.map

.aws-sam/build/FunctionC
└── dist
    ├── handlers
    │   ├── c.js
    │   └── c.js.map
    └── utils
        ├── bc.js
        └── bc.js.map

Enter fullscreen mode Exit fullscreen mode

Se mudarmos apenas o src/handlers/a.ts, apenas o recurso FunctionAserá reimplantado. E se mudarmos src/utils/bc.ts (importado nos manipuladores b e c), apenas FunctionBe FunctionC serão reimplantados. Maneiro né?

Veja o commit completo aqui .

Resumindo

  • Precisamos executar tsc -w quando codificamos, mas, bem, é inevitável e torna a vida mais divertida.
  • Nossos testes estão funcionando bem.
  • Nossos Lambdas são os menores possíveis: a pasta node_modules está dentro da camada externa compartilhada e cada Lambda inclui apenas o código real de que precisa para funcionar (entendeu o trocadilho?).
  • Podemos colocar pontos de depuração diretamente em um código TypeScript.
  • Não estamos reinventando o SAM, mas configurando-o para atender às nossas necessidades.
  • O procedimento de deploy não mudou em nada!

Essa configuração não é a ideal, mas se adapta muito bem às nossas necessidades. Se você tiver algo a acrescentar, mencione @evilmartians em um tweet (ou apenas abra um PR no repositório de exemplo).

Mostre-me seu código!

Se você é novo em lamdas, oferecemos o modelo de aplicativo SAM totalmente configurado com API Gateway, banco de dados, filas, tudo junto com algumas funções serverless CRUD de demonstração. Tudo que você precisa para experimentar é uma conta da AWS:

gh repo clone Envek/aws-sam-typescript-layers-example
sam build
sam deploy --guided
Enter fullscreen mode Exit fullscreen mode

Confira os nossos commits individuais para entender melhor as coisas.

Se você deseja apenas começar a desenvolver seus Lambdas com isso - aqui está o modelo para começar:

sam init --location gh:Envek/cookiecutter-aws-sam-typescript-layers
Enter fullscreen mode Exit fullscreen mode

E você está pronto para implantar AWS Lambda com TypeScript?

Créditos

Top comments (0)