<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Javier Toscano</title>
    <description>The latest articles on DEV Community by Javier Toscano (@javiertoscano).</description>
    <link>https://dev.to/javiertoscano</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F635923%2Fa18d862e-905a-4bac-84cb-eb5d68056fdd.png</url>
      <title>DEV Community: Javier Toscano</title>
      <link>https://dev.to/javiertoscano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javiertoscano"/>
    <language>en</language>
    <item>
      <title>Create PDF documents with AWS Lambda + S3 with NodeJS and Puppeteer</title>
      <dc:creator>Javier Toscano</dc:creator>
      <pubDate>Sun, 15 Aug 2021 06:31:25 +0000</pubDate>
      <link>https://dev.to/javiertoscano/create-pdf-documents-with-aws-lambda-s3-with-nodejs-and-puppeteer-3phi</link>
      <guid>https://dev.to/javiertoscano/create-pdf-documents-with-aws-lambda-s3-with-nodejs-and-puppeteer-3phi</guid>
      <description>&lt;p&gt;This post was originally posted on my &lt;a href="https://javtoscano.com/create-pdf-documents-with-aws-lambda-s3-with-nodejs-and-puppeteer"&gt;blog&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;Recently I had to create two serverless functions for a client that needed to create a PDF document from an existing HTML format and merge it with another PDF documents provided by users in an upload form.&lt;/p&gt;

&lt;p&gt;In this article, we will use examples based on real-world applications. &lt;br&gt;
Going through project configuration, AWS configuration, and project deployment.&lt;/p&gt;
&lt;h1&gt;
  
  
  Content
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Setting Up&lt;/li&gt;
&lt;li&gt;Setting up serverless configuration&lt;/li&gt;
&lt;li&gt;Setting up a Lambda Layer&lt;/li&gt;
&lt;li&gt;Working with Puppeteer&lt;/li&gt;
&lt;li&gt;Uploading PDF to S3&lt;/li&gt;
&lt;li&gt;Deploying to AWS&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  TL;DR:
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Lambda function &lt;a href="https://github.com/JavToscano/serverless-pdf-generator"&gt;Github Repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Login demo app &lt;a href="https://github.com/JavToscano/puppeteer-login-demo"&gt;Github Repo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Setting Up
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Serverless Framework
&lt;/h3&gt;

&lt;p&gt;We will be using the  &lt;a href="https://www.serverless.com/"&gt;Serverless Framework&lt;/a&gt; to deploy easily our resources to the cloud. &lt;/p&gt;

&lt;p&gt;Open up a terminal and type the following command to install Serverless globally using npm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g serverless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initial Project Setup
&lt;/h3&gt;

&lt;p&gt;Create a new serverless project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serverless create --template aws-nodejs --path pdf-generator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is going to create a new folder named &lt;code&gt;pdf-generator&lt;/code&gt; with two files on it &lt;code&gt;handler.js&lt;/code&gt; and &lt;code&gt;serverless.yml&lt;/code&gt;. For now, we will leave the files as-is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Dependencies.
&lt;/h3&gt;

&lt;p&gt;We will need the following dependencies to work with puppeteer on our project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;chrome-aws-lambda&lt;/strong&gt;: Chromium Binary for AWS Lambda and Google Cloud Functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;puppeteer-core&lt;/strong&gt;: Puppeteer-core is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;aws-sdk&lt;/strong&gt;: AWS SDK Library to interact with AWS Services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;serverless-webpack&lt;/strong&gt;: A Serverless v1.x &amp;amp; v2.x plugin to build your lambda functions with Webpack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;node-loader&lt;/strong&gt;: Allows to connect native node modules with .node extension.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install chrome-aws-lambda puppeteer-core
npm install -D aws-sdk node-loader serverless-webpack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuring Webpack
&lt;/h3&gt;

&lt;p&gt;Once we have our project dependencies installed, we are going to configure Webpack, to package our code and reduce the size of our cloud function, this will save us a lot of problems since lambdas can hit around 1GB of space, and sometimes AWS rejects our package because of the size.&lt;/p&gt;

&lt;p&gt;Create the file &lt;code&gt;webpack.config.js&lt;/code&gt; on our project root, and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  target: "node",
  mode: "development",
  module: {
    rules: [
      {
        test: /\.node$/,
        loader: "node-loader",
      },
    ],
  },
  externals: ["aws-sdk", "chrome-aws-lambda"],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code above we are setting the following options to Webpack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are using development mode, so our code isn't minified and we can trace errors with &lt;code&gt;AWS CloudWatch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We are importing node modules to our bundle using &lt;code&gt;node-loader&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We are excluding &lt;code&gt;aws-sdk&lt;/code&gt; and &lt;code&gt;chrome-aws-lambda&lt;/code&gt; from our bundle since AWS has a built-in &lt;code&gt;aws-sdk&lt;/code&gt; library and for &lt;code&gt;chrome-aws-lambda&lt;/code&gt; we are going to use a  &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html"&gt;Lambda Layer&lt;/a&gt;  since Webpack can't bundle the library as-is&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up serverless configuration
&lt;/h2&gt;

&lt;p&gt;Next, we are going to configure our &lt;code&gt;serverless.yml&lt;/code&gt; file, for now, we will add some environment variables, a lambda layer to use &lt;code&gt;chrome-aws-lambda&lt;/code&gt;, and add Webpack to the list of plugins.&lt;/p&gt;

&lt;p&gt;First, we define global variables to use along all of our functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;custom:
  app_url: https://puppeteer-login-demo.vercel.app
  app_user: admin@admin.com
  app_pass: 123456789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are defining custom properties that we can access in our configuration file using the syntax &lt;code&gt;${self:someProperty}&lt;/code&gt; in our case, we can access our properties using the following syntax &lt;code&gt;${self:custom.someProperty}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now we define our environment variables inside our function to allow our handler to access these variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;functions:
  generate-pdf:
    handler: handler.handler
    environment:
      APP_URL: ${self:custom.app_url}
      APP_USER: ${self:custom.app_user}
      APP_PASS: ${self:custom.app_pass}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add the plugins section at the end of our file, so we can use Webpack with our lambdas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins:
  - serverless-webpack

package:
  individually: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far our &lt;code&gt;serverless.yml&lt;/code&gt; should look like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service: pdf-generator
frameworkVersion: '2'

custom:
  app_url: https://puppeteer-login-demo.vercel.app
  app_user: admin@admin.com
  app_pass: 123456789

provider:
  name: aws
  stage: dev
  region: us-east-1
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221

functions:
  generate-pdf:
    handler: handler.handler
    environment:
      APP_URL: ${self:custom.app_url}
      APP_USER: ${self:custom.app_user}
      APP_PASS: ${self:custom.app_pass}

plugins:
  - serverless-webpack

package:
  individually: true

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting up a Lambda Layer
&lt;/h2&gt;

&lt;p&gt;To use the library &lt;code&gt;chrome-aws-lambda&lt;/code&gt; we need to use it as an external library, for this, we can create our own &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html"&gt;Lambda Layer&lt;/a&gt; or use a community hosted one.&lt;/p&gt;

&lt;p&gt;Here I'll explain both options and you can decide whenever option you want to use it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Own Hosted Layer
&lt;/h4&gt;

&lt;p&gt;First, we have to package the library as a zip file, open up the terminal, and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone --depth=1 https://github.com/alixaxel/chrome-aws-lambda.git &amp;amp;&amp;amp; \
cd chrome-aws-lambda &amp;amp;&amp;amp; \
make chrome_aws_lambda.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above will create a &lt;code&gt;chrome-aws-lambda.zip&lt;/code&gt; file, which can be uploaded to your Layers console.&lt;/p&gt;

&lt;h4&gt;
  
  
  Community Hosted Layer
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/shelfio/chrome-aws-lambda-layer"&gt;This repository&lt;/a&gt; hosts a Community Lambda Layer so we can use it directly on our function. At this time the latest version is &lt;code&gt;24&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have to add this layer to our &lt;code&gt;serverless.yml&lt;/code&gt; file and specify that our function is going to use this layer, in this case, we are going to use the community version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;functions:
  generate-pdf:
    handler: handler.handler
    layers:
      - arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working with Puppeteer
&lt;/h2&gt;

&lt;p&gt;Now that our project is configured, we are ready to start developing our lambda function.&lt;/p&gt;

&lt;p&gt;First, we start loading the chromium library and creating a new instance in our &lt;code&gt;handler.js&lt;/code&gt; file to work with Puppeteer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";
const chromium = require("chrome-aws-lambda");

exports.handler = async (event, context) =&amp;gt; {
  let browser = null;

  try {
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
      ignoreHTTPSErrors: true,
    });

    const page = await browser.newPage();
  } catch (e) {
    console.log(e);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we will use an app that needs login to view the report that we want to convert to PDF, so first, we are going to navigate to the login page and using the environment variables to simulate a login to access the report.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    await page.goto(`${process.env.APP_URL}/login`, {
      waitUntil: "networkidle0",
    });
    await page.type("#email", process.env.APP_USER);
    await page.type("#password", process.env.APP_PASS);
    await page.click("#loginButton");
    await page.waitForNavigation({ waitUntil: "networkidle0" });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code we carry out the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the login page&lt;/li&gt;
&lt;li&gt;Search for the input with ID &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; and type the user and password credentials from the env variables.&lt;/li&gt;
&lt;li&gt;Click on the button with ID &lt;code&gt;loginButton&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait for the next page to be fully loaded (in our example we are being redirected to a Dashboard)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we are logged in, so we can navigate to the report URL that we want to convert to a PDF file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    await page.goto(`${process.env.APP_URL}/invoice`, {
      waitUntil: ["domcontentloaded", "networkidle0"],
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we go to the &lt;code&gt;invoice&lt;/code&gt; page and wait until the content is fully loaded.&lt;/p&gt;

&lt;p&gt;Now that we are on the page that we want to convert, we create our PDF file and save it on the &lt;code&gt;buffer&lt;/code&gt; to save it later to AWS S3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      const buffer = await page.pdf({
        format: "letter",
        printBackground: true,
        margin: "0.5cm",
      });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in the above code we added a few options to the &lt;code&gt;pdf&lt;/code&gt; method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;format&lt;/strong&gt;: the size of our file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;printBackground&lt;/strong&gt;: print background graphics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;margin&lt;/strong&gt;: add a margin of 0.5cm to the print area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far our &lt;code&gt;handler.js&lt;/code&gt; should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";
const chromium = require("chrome-aws-lambda");

exports.handler = async (event, context) =&amp;gt; {
  let browser = null;

  try {
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
      ignoreHTTPSErrors: true,
    });

    const page = await browser.newPage();

    await page.goto(`${process.env.APP_URL}/login`, {
      waitUntil: "networkidle0",
    });
    await page.type("#email", process.env.APP_USER);
    await page.type("#password", process.env.APP_PASS);
    await page.click("#loginButton");
    await page.waitForNavigation({ waitUntil: "networkidle0" });

    await page.goto(`${process.env.APP_URL}/invoice`, {
      waitUntil: ["domcontentloaded", "networkidle0"],
    });

    const buffer = await page.pdf({
      format: "letter",
      printBackground: true,
      margin: "0.5cm",
    });
  } catch (e) {
    console.log(e);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Uploading PDF to S3
&lt;/h2&gt;

&lt;p&gt;Currently, we can generate our PDF file using Puppeteer, now we are going to configure our function to create a new S3 Bucket, and upload our file to S3.&lt;/p&gt;

&lt;p&gt;First, we are going to define in our &lt;code&gt;serverless.yml&lt;/code&gt; file, the resources for the creation and usage of our S3 bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service: pdf-generator
frameworkVersion: '2'

custom:
  app_url: https://puppeteer-login-demo.vercel.app
  app_user: admin@admin.com
  app_pass: 123456789
  bucket: pdf-files

provider:
  name: aws
  stage: dev
  region: us-east-1
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:PutObject
            - s3:PutObjectAcl
          Resource: "arn:aws:s3:::${self:custom.bucket}/*"
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221

functions:
  generate-pdf:
    handler: handler.handler
    timeout: 25
    layers:
      - arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:24
    environment:
      APP_URL: ${self:custom.app_url}
      APP_USER: ${self:custom.app_user}
      APP_PASS: ${self:custom.app_pass}
      S3_BUCKET: ${self:custom.bucket}

plugins:
  - serverless-webpack

package:
  individually: true

resources:
  Resources:
    FilesBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.bucket}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we defined our resource &lt;code&gt;FilesBucket&lt;/code&gt; that Serverless is going to create, and we also defined the permissions that our Lambda has over the Bucket, for now, we just need permission to put files.&lt;/p&gt;

&lt;p&gt;Now in our &lt;code&gt;handler.js&lt;/code&gt; we load the AWS library and instance a new S3 object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const AWS = require("aws-sdk");
const s3 = new AWS.S3({ apiVersion: "2006-03-01" });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we just need to save our &lt;code&gt;buffer&lt;/code&gt; variable to our S3 Bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    const s3result = await s3
      .upload({
        Bucket: process.env.S3_BUCKET,
        Key: `${Date.now()}.pdf`,
        Body: buffer,
        ContentType: "application/pdf",
        ACL: "public-read",
      })
      .promise();

    await page.close();
    await browser.close();

    return s3result.Location;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we uploaded our file to our Bucket, closed our &lt;code&gt;chromium&lt;/code&gt; session, and returned the new file URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to AWS
&lt;/h2&gt;

&lt;p&gt;First, we need to add our AWS Credentials to Serverless in order to deploy our functions, please visit the &lt;a href="https://www.serverless.com/framework/docs/providers/aws/guide/credentials/"&gt;serverless documentation&lt;/a&gt; to select the appropriate auth method for you.&lt;/p&gt;

&lt;p&gt;Now, open the &lt;code&gt;package.json&lt;/code&gt; file to add our deployment commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  "scripts": {
    "deploy": "sls deploy",
    "remove": "sls remove"
  },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we added 2 new commands, &lt;code&gt;deploy&lt;/code&gt; and &lt;code&gt;remove&lt;/code&gt;, open up a terminal and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our function is bundled and deployed to AWS Lambda!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
