This post assumes that you have basic knowledge of the Serverless framework and AWS Lambda, have created an AWS account with a lambda function initialized. This article will focus on going over the code used to generate a PDF file using user input received from a client and an HTML template, then store it in an AWS s3 bucket.
Make sure that you have added your AWS credentials to serverless, you can do so from the CLI as so:
export AWS_ACCESS_KEY_ID=<your-key-here>
export AWS_SECRET_ACCESS_KEY=<your-secret-key-here>
# AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are now available for serverless to use
serverless deploy
# 'export' command is valid only for unix shells. In Windows - use 'set' instead of 'export'
First things first, let's create a directory:
mkdir lambdas && cd lambdas
Note:
I have this trusty alias in my ~/.zshrc file that I use to create directories and cd into them:
function mkcd {
local target=$1
mkdir -p "$target"
cd $target
}
Let's install our dependencies:
npm install aws-sdk chrome-aws-lambda puppeteer-core
-
aws-sdk
will help use connect to AWS and make use of their services such as lambda and s3! -
chrome-aws-lambda
&puppeteer-core
will allow us to spin up a headless version of chrome. We will use this instance to create a blank page, add the content, and store it as a PDF
** Make sure you have the serverless framework installed globally **
(npm install -g serverless
)
Once inside our directory or your location of choice, we can go ahead and leverage the serverless template scaffolding to create a new handler.js
using the nodejs blueprint.
serverless create --template aws-nodejs --name generatePdf
This will create 3 files for us:
handler.js
serverless.yml
.gitignore
The first thing I did was to go ahead and clean up all of the contents in the serverless.yml
file and only left what was necessary for us to be able to perform the desired action.
After some cleanup, my serverless.yml
file looks like this:
service: pdf
frameworkVersion: "2"
provider:
name: aws
runtime: nodejs12.x
region: us-east-1
functions:
handler:
handler: handler.handler # π this access the handler
memorySize: 1600 # here we tell AWS how much memory to allocate
timeout: 30
events:
- http:
path: users/create # π this defines the http path
method: get
cors: true
package:
exclude:
- node_modules/puppeteer/.local-chromium/**
At the time of writing this, the handler.js
file should initially look like this:
'use strict';
module.exports.hello = async event => {
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
},
null,
2
),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
At this point, none of this is relevant to our goal. We will go ahead and clear everything up, install dependencies and get to the nitty gritty!
If you recall, in our config file, we said that the path to our module would be called handler and that lived within handler.js
functions:
handler:
handler: handler.handler // π this
memorySize: 1600
timeout: 30
It really doesn't matter what you call your handler, just make sure that the name is the same in both your config file and your exported module.
module.exports.handler = async event => {
// .. cleaned up content and changed name of
// exported module from hello to handler
};
Now, let's import and initialize our S3 instance from the aws-sdk
package we installed earlier.
"use strict"
const { S3 } = require("aws-sdk");
const s3 = new S3(); // π initialize our instance
module.exports.handler = async event => {
// .. cleaned up content and changed name of
// exported module from hello to handler
};
Let's also add the rest of the utilities from chrome-aws-lamda
and add some initial boilerplate code.
"use strict"
const { S3 } = require("aws-sdk");
const { puppeteer, args, defaultViewport, executablePath } = require("chrome-aws-lambda");
const s3 = new S3(); // π initialize our instance
module.exports.handler = async (event, ctx, cb) => {
let result = null;
let browser = null;
const date = new Date().toISOString(); // we will use this
// to create filename
const filename = `pdf-${date}` // you can call this whatever you want
// but make it unique or else the file
// will be replaced
const pdfPath = `/tmp/${filename}.pdf`
// I will pause here to further talk about tmp files
};
I want to make a quick note about /tmp/
files in case you are not familiar with them. As the shorthand notation might infer, it refers to a temporary file. These files are typically created by an application to store some form of temporary data.
In a nutshell, whenever we run our function it will generate this temporary directory and once the execution of the function is done, it will discard it. This provides the perfect environment needed to create a pdf file and store it in S3(more permanent storage).
Back from our short break and let's start writing some funky logic.
module.exports.handler = async event => {
let result = null;
let browser = null;
const date = new Date().toISOString(); filename
const filename = `pdf-${date}`
const pdfPath = `/tmp/${filename}.pdf`
try {
console.log("Establishing connection...");
// Initialize and launched puppeteer
browser = await puppeteer.launch({
args,
defaultViewport,
executablePath: await executablePath,
headless: true, // πVery important, remember we want to run headless chrome
ignoreHTTPSErrors: true,
});
console.log("Opening new page...");
// π create a new headless chrome pag
const page = await browser.newPage();
console.log("Generating PDF file from HTML template...");
// π Ignore this line for right now
await page.setContent('<h1>Hello world!</h1>', { waitUntil: "networkidle2" });
// π this tells puppeteer to save the webpage as a pdf file
await page.pdf({ format: "Letter", path: pdfPath });
const params = {
Key: pdfPath,
Body: fs.createReadStream(pdfPath),
Bucket: "<yourS3Bucket>",
ContentType: "application/pdf",
};
console.log("Uploading PDF...");
// π Pretty self explanatory but this is what uploads
// and store our PDF or file to S3
await s3
.upload(params, async (err, res) => {
if (err) {
console.log(err);
throw new Error(err);
}
console.log("done");
console.log(res);
return cb(null, res);
})
.promise();
result = await page.title();
} catch (error) {
return cb(error);
} finally {
if (browser !== null) {
console.log("Closing browser...");
await browser.close();
}
}
return cb(null, result);
};
So! This is pretty much it for the handler itself, but we are still missing something very important! We are missing our HTML template, so go ahead and create a new file inside your directory called template.js
(or whatever you want to call it).
Wait! .js
file? But I thought we were creating an HTML template!
And yes, you are correct we are creating an HTML template, BUT because we want to be able to add dynamic fields to that template we will return the HTML content from a function. This way we can add variables and interpolate them into our HTML. JS FTW!
From your root directory, run touch template.js
and enter the following contents to give you an example of how it would be done:
module.exports.template = ({ someVariable }) => {
const today = new Date();
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>PDF Result Template</title>
<style>
.container {
background-color: rebbeccapurble
}
</style>
</head>
<body>
<div class="container">
Hello ${someVariable}!
Today's date is: ${today}
</div>
</body>
</html>
`;
};
Up until right now, our serverless function should be able to generate a mostly blank PDF doc with Hello World in it. That isn't dynamic enough though.
To fix this we can import our template.js
module into our function and call it from the page set content function.
Inside our handler, we had a comment that said to ignore the below-called function. Well, let's give it some attention now.
In await page.setContent('<h1>Hello world!</h1>', { waitUntil: "networkidle2" })
Replace the first string argument with the important function and pass it some data.
// ... other imports
const { template } = require("./template");
// destructure π data from event argument
module.exports.handler = async({ data }, ctx, cb) => {
// ...
//
await page.setContent(template({ ...data }), { waitUntil: "networkidle2" });
// ...
};
page.setContent
could be considered the main bread and butter since this is what merges our blank page and our template to render the desired outcome.
We can test if this works locally by running the following command:
serverless invoke local --function functionName --data '{"data": "Alejandro"}'
Finally, you can run serverless deploy
!
Thanks for reading, any feedback and/or improvements are more than welcome :)
Top comments (1)
for some reason, setContent of html with script tags does not wait for network requests to return even when I add waitUntil. any idea why?