Opa! Você que já é do mundo do Javascript deve em algum momento ter usado webpack pra empacotar as coisas e reduzir tamanho, separar em chunks e por ai vai. A ideia neste post é exatamente essa: otimizar as nossas lambdas para terem um menor tamanho que vai impactar em um menor cold start (falamos disso mais pra frente). Quer saber como usar o webpack com o serverless? BORA!

TL;DR; Repo: https://github.com/devprimata/post-serverless-webpack

Iniciando o projeto

Vamos começar um projeto serverless do zero então segue o passo a passo:

Com isso temos o projeto iniciado vamos instalar as dependências que precisamos:

Se você usa npm:

npm install --save-dev serverless-webpack webpack babel-loader @babel/core

Se você usa yarn:

yarn add -D serverless-webpack webpack babel-loader @babel/core

Configurando o serverless framework

Agora com tudo instalado vamos configurar o serverless framework para utilizar o plugin serverless-webpack que nós instalamos. Eu limpei o arquivo serverless.yml pra ficar mais simples:

service: serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: us-east-2

functions:
  hello:
    handler: handler.hello

Vamos adicionar a chamada de plugin ficando assim:

service: serverless-webpack

plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: us-east-2

functions:
  hello:
    handler: handler.hello

Ok! O plugin já vai responder as chamadas do serverless mas antes precisamos configurar o webpack do modo normal, ou seja, criando o webpack.config.js na raiz do projeto com o conteúdo:

module.exports = {
  entry: "./handler.js",
  mode: "development"
};

Como que a gente faz para testar isso? Roda um sls package pra vc ver!

Esse comando empacota nossas lambdas e o output dele vai ser em:

.serverless/serverless-webpack.zip.

Se você extrair o conteúdo vai ver o main.js lá dentro certinho.

Agora vamos melhorar isso colocando o babel-loader. No webpack.config.js altere para algo assim:

const path = require("path");

module.exports = {
  entry: "./handler.js",
  mode: "development",
  target: "node",
  devtool: "source-map",
  output: {
    libraryTarget: "commonjs",
    path: path.join(__dirname, ".webpack"),
    filename: "[name].js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: __dirname,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      }
    ]
  }
};

Legal! Agora tudo está passando pelo babel porém você pode me perguntar: E como faço se eu tenho várias lambdas mapeadas no serverless.yml? Boa pergunta! Vamos lá.

Pra mapear vários entrypoints vamos alterar o serverless.yml e adicionar uma nova função:

service: serverless-webpack

plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: us-east-2

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: post
          path: hello/

  consumer:
    handler: consumer.main
    events:
      - http:
          method: post
          path: consumer/

Não pira não, tá? Só adicionei a função consumer e as entradas pelo API Gateway pra caso você quiser testar já está pronto, bele?

Aí falta alterar o webpack.config.js:

const path = require("path");
const slsWebpack = require("serverless-webpack");

module.exports = {
  entry: slsWebpack.lib.entries,
  mode: slsWebpack.lib.webpack.isLocal ? "development" : "production",
  target: "node",
  devtool: "source-map",
  output: {
    libraryTarget: "commonjs",
    path: path.join(__dirname, ".webpack"),
    filename: "[name].js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: __dirname,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      }
    ]
  }
};

Dois pontos importantes foram alterados:

entry: slsWebpack.lib.entries,

Nessa linha a gente mostra pro webpack que as entries estão vindo diretamente do serverless-webpack que se comunica com o serverless.


Nessa: mode: slsw.lib.webpack.isLocal ? "development" : "production", a gente só segmenta o modo como vai ser feito o package pra ajudar a gente com debug local e em produção otimizar.

Bora testar novamente, roda um sls package e veja o conteúdo do .zip.

Deploy

Se você fizer o deploy desse código vai ver algo estranho: sls deploy

Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service serverless-webpack.zip file to S3 (5.37 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...

Sacou? Não?!

Serverless: Uploading service serverless-webpack.zip file to S3 (5.37 KB)...

via GIPHY

Ele empacotou tudo no mesmo zip e não é bem isso que a gente quer, queremos um zip otimizado para cada lambda, pra isso acontecer é só alterar o serverless.yml:

service: serverless-webpack

plugins:
  - serverless-webpack

package:
  individually: true
  
...

Adicionando o package: individually: true o serverless vai enteder que precisa criar um zip para cada coisa, vamos ao deploy novamente sls deploy:

Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service hello.zip file to S3 (2.74 KB)...
Serverless: Uploading service consumer.zip file to S3 (2.65 KB)...
Serverless: Validating template...
Serverless: Updating Stack...

Agora sim! Tudo certo.

Essa é a ideia de usar o serverless-webpack, espero que tenha ficado claro, qualquer dúvida manda nos comentários! Aproveita e já de inscreve na newsletter!

Faaaaaaaaaalou!