Genera PDFs en AWS Lambda con Node.js y Puppeteer: Una Guía Completa

Frank Leonardo Puma Chara
Pragma
Published in
8 min readAug 6, 2024

En este artículo explicaremos cómo generar archivos pdfs usando Puppeteer, y desplegarlo en el servicio AWS Lambda usando Serverless Framework y Lambda Layers.

Introducción

En algún momento, nos hemos enfrentado al desafío de crear un servicio para generar PDFs. Hoy en día, esta tarea se ha vuelto más accesible y eficiente gracias a la arquitectura serverless. En este artículo explicaremos cómo usar una función AWS Lambda utilizando el Framework Serverless y Puppeteer para generar PDFs de manera automática. También hablaremos de un enfoque de entorno productivo con Lambda Layers. Esta aproximación no solo optimiza el uso de recursos y costos, sino que también ofrece una escalabilidad y flexibilidad impresionantes.

Requisitos

  • Conocimientos en Typescript.
  • Conocimientos en AWS Lambda, AWS S3 y Serverless Framework.

Objetivo

En este artículo, nos proponemos introducir a los lectores en el uso de Puppeteer para la generación de PDFs, guiarlos en la configuración y despliegue de una función AWS Lambda utilizando Serverless Framework, y mostrar cómo implementar una solución escalable y eficiente en producción mediante el uso de Lambda Layers y Amazon S3 para almacenar los PDFs generados.

Estructura del proyecto

Trabajaremos con la siguiente estructura de proyecto:

aws-lambda-puppeteer/

├── src/
│ └── handler.ts

├── dist/
│ └── (archivos compilados)

├── package.json
├── README.md
├── serverless.yml
└── tsconfig.json

Instalación

npm i @aws-sdk/client-s3@3.272.0 @sparticuz/chromium@126.0.0 puppeteer-core@22.14.0
npm i -D serverless@3.39.0 typescript

Configuración de Scripts

El siguiente script nos ayudará a generar el código Javascript en la carpeta “dist” utilizado por serverless framework para desplegarlo en AWS.

"deploy": "npx tsc && npx sls deploy"

Handler.ts

Nuestro punto de partida será crear el archivo handler.ts donde se describe la función lambda.

A continuación definimos la configuración necesaria para ejecutar puppeteer, donde es importante “executablePath” la cual es una ruta que indica la ubicación de los archivos binarios de chromium. Esta configuración permite que puppeteer sepa exactamente dónde encontrar el navegador para poder ejecutarlo. Puedes encontrar más detalles sobre la configuración de Puppeteer en la documentación oficial aqui.

// config browser
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath('https://github.com/Sparticuz/chromium/releases/download/v123.0.1/chromium-v123.0.1-pack.tar'),
headless: chromium.headless,
})

Seguidamente definimos el contenido que queremos mostrar en nuestro PDF en formato HTML, que en este caso será la frase “Hello world!” en color rojo.

// create new page and set content
const page = await browser.newPage()
await page.setContent(`
<div style="color: red">
Hello world!
</div>
`)

Hasta este punto ya tenemos lo necesario para generar el archivo y en este caso como queremos mostrar un resultado, generamos un Buffer que se usará junto con el servicio S3 para almacenarlo.

// generate pdf Buffer, store the file in S3 for example
const pdfBuffer: Buffer = await page.pdf({format: 'a4'})

// upload file to S3
const s3 = new S3Client({region: 'us-east-1'})
await s3.send(new PutObjectCommand({
Bucket: 'aws-lambda-puppeteer-tutorial',
Key: 'my-doc.pdf',
Body: pdfBuffer,
}))

Finalmente liberamos los recursos y retornamos una respuesta exitosa.

// close the browser
await browser.close()

console.log('browser is closed!')
return {
statusCode: 200,
body: 'Pdf Generated!',
headers: {
'Content-Type': 'application/json'
}
}

Así quedaría el código completo del archivo handler.ts:

import chromium from '@sparticuz/chromium'
import puppeteer from 'puppeteer-core'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { APIGatewayEventRequestContext, APIGatewayProxyEvent } from 'aws-lambda'

export const generatePDF = async (event: APIGatewayProxyEvent, context: APIGatewayEventRequestContext) => {

// config browser
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath('/opt/nodejs/chromium'),
headless: chromium.headless,
})

console.log('browser is up')

// create new page and set content
const page = await browser.newPage()
await page.setContent(`
<div style="color: red">
Hello world!
</div>
`)

// generate pdf Buffer, store the file in S3 for example
const pdfBuffer: Buffer = await page.pdf({format: 'a4'})

// upload file to S3
const s3 = new S3Client({region: 'us-east-1'})
await s3.send(new PutObjectCommand({
Bucket: 'aws-lambda-puppeteer-tutorial',
Key: 'my-doc.pdf',
Body: pdfBuffer,
}))

// close the browser
await browser.close()

console.log('browser is closed!')
return {
statusCode: 200,
body: 'Pdf Generated!',
headers: {
'Content-Type': 'application/json'
}
}
}

A continuación tenemos el archivo serverless.yml, en el que especificamos la ruta del handler para nuestra función lambda, la versión del runtime de node con el que se ejecutará y los permisos que tendrá sobre nuestro bucket de S3.

service: aws-lambda-puppeteer
frameworkVersion: '3'

provider:
name: aws
runtime: nodejs18.x
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
Resource: arn:aws:s3:::aws-lambda-puppeteer-tutorial/*

functions:
generate-pdf:
handler: dist/handler.generatePDF
timeout: 300

Compilación y despliegue

Para el despliegue y probar la función utilizaremos el script anteriormente configurado de la siguiente forma:

npm run deploy

Prueba y Resultado

Ingresamos a la consola de AWS, identificamos la función y probamos la funcionalidad dando click a “Test”

Luego nos dirigimos al servicio S3 donde encontraremos el archivo pdf con el resultado 🚀.

Entornos de producción

Actualmente el funcionamiento del código es que se descarga una versión binaria de chromium desde Github lo cual funciona correctamente de forma local, pero hay que considerar que en un entorno de producción la imagen binaria podría darse de baja y no estar disponible por lo cual hay que tomar ciertas consideraciones al momento de desplegar una solución sería guardar el binario en un bucket de S3 y otra usar Lambda Layers que es una forma de compartir código entre funciones lambda.

Entornos de producción — Lambda Layers

Para poder desplegar una Layer nos apoyaremos de Serverless Framework el cual se encargará de empaquetar el código y desplegarlo en AWS, pero hay que considerar cierta estructura de carpetas.

aws-lambda-puppeteer/

├── layer/
│ └── nodejs/
│ └── chromium/
│ ├── al2.tar.br
│ ├── al2023.tar.br
│ ├── chromium.br
│ ├── fonts.tar.br
│ └── swiftshader.tar.br

├── src/
│ └── handler.ts

├── dist/
│ └── (archivos compilados)

├── package.json
├── README.md
├── serverless.yml
└── tsconfig.json

Y modificamos el archivo serveless.yml agregando la definición de la capa (en inglés Layer) que usará nuestra función lambda donde “path” representa la ruta raiz que serverless framework usará para construir la capa:

layers:
Chrome:
path: layer
description: Chrome Binary

Luego enlazamos de la siguiente forma la definición de la capa a nuestra función:

layers:
- { Ref: ChromeLambdaLayer }

Finalmente el resultado final del archivo serverless.yml sería el siguiente:

service: aws-lambda-puppeteer
frameworkVersion: '3'

provider:
name: aws
runtime: nodejs18.x
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
Resource: arn:aws:s3:::aws-lambda-puppeteer-tutorial/*

layers:
Chrome:
path: layer
description: Chrome Binary

functions:
generate-pdf:
handler: dist/handler.generatePDF
layers:
- { Ref: ChromeLambdaLayer }
timeout: 300

Luego en el archivo handler.ts modificar la siguiente línea:

executablePath: await chromium.executablePath('/opt/nodejs/chromium'),

Realizar el mismo proceso de despliegue e ingresar nuevamente a la consola de AWS donde veremos que se está usando una Layer.

Estilos — Margen

Adicionalmente, para poder presentar nuestro archivo PDF de forma más ordenada, podemos agregar márgenes al archivo. Los márgenes son espacios en blanco que se añaden alrededor del contenido del documento, ayudando a mejorar la presentación y la legibilidad. Podemos configurar los márgenes superior, inferior, izquierdo y derecho según nuestras necesidades, a continuación se muestran valores aproximados similares a los que manejan los navegadores web actuales:

// generate pdf Buffer
const pdfBuffer: Buffer = await page.pdf({
format: 'a4',
margin: {
top: '0.4in',
left: '0.4in',
right: '0.4in',
bottom: '0.4in',
}
})

Tamaños de Hoja

Este servicio también nos permite definir ciertos tamaños de hoja para diversos casos de uso. Seleccionar el tamaño de hoja adecuado es crucial para asegurarse de que el contenido se presente correctamente y cumpla con los estándares requeridos. Puedes encontrar los tamaños de hoja más comunes y sus dimensiones aqui.

// format: 'Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6'
const pdfBuffer: Buffer = await page.pdf({
format: 'A4',
margin: {
top: '0.4in',
left: '0.4in',
right: '0.4in',
bottom: '0.4in',
}
})

Formato Landscape

El formato landscape (horizontal) es útil cuando se necesita más espacio horizontal que vertical, como en gráficos, tablas extensas, y presentaciones visuales. Cambiar la orientación de la página a landscape puede mejorar la legibilidad y presentación de ciertos tipos de contenido.

Para generar un documento en formato landscape usando Puppeteer, podemos configurar la opción landscape en true dentro del método pdf. A continuación, se muestra un ejemplo de cómo configurar esto:

// generate pdf Buffer
const pdfBuffer: Buffer = await page.pdf({
format: 'a4',
landscape: true,
margin: {
top: '0.4in',
left: '0.4in',
right: '0.4in',
bottom: '0.4in',
}
})

Formato Portrait

El formato portrait (vertical) es el más común y se utiliza ampliamente para documentos de texto, informes, cartas, y la mayoría de los documentos estándar. La orientación portrait es ideal cuando se necesita más espacio vertical que horizontal, proporcionando una presentación más tradicional y fácil de leer para la mayoría de los contenidos.

Para generar un documento en formato portrait usando Puppeteer, podemos configurar la opción landscape en false o simplemente no incluirla, ya que el valor predeterminado es portrait. A continuación, se muestra un ejemplo de cómo configurar esto:

// generate pdf Buffer
const pdfBuffer: Buffer = await page.pdf({
format: 'a4',
landscape: false,
margin: {
top: '0.4in',
left: '0.4in',
right: '0.4in',
bottom: '0.4in',
}
})

Conclusión

Hemos visto cómo implementar una función lambda en AWS que nos permite generar archivos PDF de forma muy sencilla, así como una versión mejorada para entornos de producción.

Por si estás interesado en tener un mayor detalle puedes encontrar el código completo en Github.

Referencias

  1. Documentación oficial de Puppeteer
  2. Documentación oficial del paquete Sparticuz Chromium
  3. Documentación oficial de Serverless Framework para usar Lambda Layers
  4. Documentación oficial y consideraciones a tomar de AWS Lambda Layers

¡Gracias por leer! Espero que este artículo te haya sido útil. ¡Hasta la próxima! 😉

--

--