├── .gitignore ├── LICENSE ├── README.md ├── examples ├── deploy_cli.png ├── logo.png ├── test.js ├── test.pdf └── test.png ├── index.js ├── package.json ├── pdfrender-_star_-_star_-policy.json └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .DS_Store 4 | .serverless 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/deepsyx/simple-headless-browser-serverless/blob/master/examples/logo.png?raw=true) 2 | 3 | The goal of this repo is to provide a basic example of how to get headless browser running on AWS at scale. 4 | 5 | How it works 6 | === 7 | AWS Lambda lets you run code without provisioning or managing servers. You pay only for the compute time you consume. Basically, you set the required hardware parameters of your code and upload the bundle to AWS and they handle the rest. Let's say one HTML to PDF takes 2 seconds to complete while using 100% CPU. If we upload our code to a single-core instance, we'll be limited to 1 render per 2 seconds and you'll be still paying for the instance time while idle. However, with AWS lambda you can run your code unlimited times simultaneously and it'll be executed in parallel. We'll be using the serverless framework to deploy our bundle - it makes the whole process as easy as executing a single command. 8 | 9 | "Puppeteer is a Node library that provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium." 10 | 11 | Since AWS Lambdas is running on Amazon Linux, we'll have to use a Chrome compiled for Amazon Linux, so we use the `chrome-aws-lambda` package which is providing the ready-to-use binaries. 12 | 13 | How to deploy this repo 14 | ===== 15 | 16 | Setup your [AWS credentials](https://serverless.com/framework/docs/providers/aws/guide/credentials/) then run: 17 | 18 | npm install serverless -g 19 | git clone git@github.com:deepsyx/serverlessdemo.git 20 | cd ./serverlessdemo 21 | yarn 22 | serverless deploy 23 | 24 | And you should see something like: 25 | ![deploy image](https://github.com/deepsyx/simple-headless-browser-serverless/blob/master/examples/deploy_cli.png?raw=true) 26 | 27 | Example 28 | ===== 29 | 30 | const https = require("https"); 31 | const fs = require("fs"); 32 | 33 | const data = JSON.stringify({ url: "https://example.com", format: "png" }); 34 | 35 | const options = { 36 | hostname: "5zxn2gg42h.execute-api.us-east-1.amazonaws.com", 37 | port: 443, 38 | path: "/dev/", 39 | method: "POST", 40 | headers: { 41 | "Content-Type": "application/json", 42 | "Content-Length": data.length 43 | } 44 | }; 45 | 46 | const req = https.request(options, res => { 47 | const buffer = []; 48 | 49 | res.on("data", d => { 50 | buffer.push(d); 51 | }); 52 | 53 | res.on("end", d => { 54 | const extension = res.headers["content-type"].includes("/pdf") 55 | ? "pdf" 56 | : "png"; 57 | 58 | const fileName = `./test.${extension}`; 59 | fs.writeFileSync(fileName, Buffer.concat(buffer)); 60 | console.log(`Wrote file: ${fileName}`); 61 | }); 62 | }); 63 | 64 | req.on("error", error => { 65 | console.error(error); 66 | }); 67 | 68 | req.write(data); 69 | req.end(); 70 | 71 | Request properties 72 | ====== 73 | 74 | | Property | Type | Example | Description 75 | | ------------- |:-------------:| -----:| -----:| 76 | | url | String(Optional) | "https://example.com" | URl to be screenshoted 77 | | html | String(Optional) | "Hello world!" | If we don't want to screenshot a url, we can manually provide the html 78 | | format | String | "pdf" or "png" | Specifies the output format 79 | | viewport | Object (optional) | viewport: { width: 1920, height: 1080 } | Specifies the browser viewport 80 | 81 | 82 | Last words 83 | ====== 84 | 85 | Feel free to fork this repo and extend it with your own functionally, if you find it useful. Improvement pull requests are welcome, as long as they're well formatted and prettified with `yarn prettify`. 86 | -------------------------------------------------------------------------------- /examples/deploy_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepsyx/simple-headless-browser-serverless/4312fd52b7efe76cb2b20da7a39a1a1e2e09ec68/examples/deploy_cli.png -------------------------------------------------------------------------------- /examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepsyx/simple-headless-browser-serverless/4312fd52b7efe76cb2b20da7a39a1a1e2e09ec68/examples/logo.png -------------------------------------------------------------------------------- /examples/test.js: -------------------------------------------------------------------------------- 1 | const https = require("https"); 2 | const fs = require("fs"); 3 | 4 | const data = JSON.stringify({ url: "https://example.com", format: "png" }); 5 | 6 | const options = { 7 | hostname: "5zxn2gg42h.execute-api.us-east-1.amazonaws.com", 8 | port: 443, 9 | path: "/dev/", 10 | method: "POST", 11 | headers: { 12 | "Content-Type": "application/json", 13 | "Content-Length": data.length 14 | } 15 | }; 16 | 17 | const req = https.request(options, res => { 18 | const buffer = []; 19 | 20 | res.on("data", d => { 21 | buffer.push(d); 22 | }); 23 | 24 | res.on("end", d => { 25 | const extension = res.headers["content-type"].includes("/pdf") 26 | ? "pdf" 27 | : "png"; 28 | 29 | const fileName = `./test.${extension}`; 30 | fs.writeFileSync(fileName, Buffer.concat(buffer)); 31 | console.log(`Wrote file: ${fileName}`); 32 | }); 33 | }); 34 | 35 | req.on("error", error => { 36 | console.error(error); 37 | }); 38 | 39 | req.write(data); 40 | req.end(); 41 | -------------------------------------------------------------------------------- /examples/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepsyx/simple-headless-browser-serverless/4312fd52b7efe76cb2b20da7a39a1a1e2e09ec68/examples/test.pdf -------------------------------------------------------------------------------- /examples/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepsyx/simple-headless-browser-serverless/4312fd52b7efe76cb2b20da7a39a1a1e2e09ec68/examples/test.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const chromium = require("chrome-aws-lambda"); 4 | 5 | const DEFAULT_OPTIONS = { 6 | viewport: { 7 | width: 1920, 8 | height: 1080 9 | }, 10 | format: "pdf" 11 | }; 12 | 13 | module.exports.main = async (event, context, callback) => { 14 | let result = null; 15 | let browser = null; 16 | 17 | const options = { 18 | ...DEFAULT_OPTIONS, 19 | ...JSON.parse(Buffer.from(event.body, "base64").toString()) 20 | }; 21 | 22 | try { 23 | browser = await chromium.puppeteer.launch({ 24 | args: chromium.args.concat([ 25 | "--allow-file-access-from-files", 26 | "--enable-local-file-accesses", 27 | `--window-size=${options.viewport.width}x${options.viewport.height}`, 28 | "--font-render-hinting=none" 29 | ]), 30 | defaultViewport: options.viewport, 31 | executablePath: await chromium.executablePath, 32 | headless: true 33 | }); 34 | 35 | let page = await browser.newPage(); 36 | await page.emulateMedia("screen"); 37 | 38 | const url = options.html ? `data:text/html,${options.html}` : options.url; 39 | 40 | await page.goto(url, { 41 | waitUntil: "networkidle0" 42 | }); 43 | 44 | if (options.format === "png") { 45 | result = await page.screenshot({ 46 | encoding: "base64" 47 | }); 48 | } else if (options.format === "pdf") { 49 | result = Buffer.from(await page.pdf()).toString("base64"); 50 | } else { 51 | throw new Error("Invalid format specified."); 52 | } 53 | 54 | await browser.close(); 55 | } catch (error) { 56 | if (browser) { 57 | await browser.close(); 58 | } 59 | 60 | callback(null, { 61 | statusCode: 500, 62 | body: error 63 | }); 64 | return; 65 | } 66 | 67 | const contentType = 68 | options.format === "png" ? "image/png" : "application/pdf"; 69 | 70 | callback(null, { 71 | statusCode: 200, 72 | headers: { 73 | "Content-Type": contentType, 74 | "Access-Control-Allow-Origin": "*", 75 | "Access-Control-Allow-Credentials": true 76 | }, 77 | body: result, 78 | isBase64Encoded: true 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdfrender", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prettify": "node ./node_modules/prettier/bin-prettier --write **.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "chrome-aws-lambda": "2.0.2", 14 | "puppeteer-core": "2.1.0", 15 | "serverless": "1.65.0", 16 | "serverless-apigw-binary": "0.4.4" 17 | }, 18 | "devDependencies": { 19 | "prettier": "^1.19.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pdfrender-_star_-_star_-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "cloudformation:List*", 8 | "cloudformation:Get*", 9 | "cloudformation:ValidateTemplate" 10 | ], 11 | "Resource": [ 12 | "*" 13 | ] 14 | }, 15 | { 16 | "Effect": "Allow", 17 | "Action": [ 18 | "cloudformation:CreateStack", 19 | "cloudformation:CreateUploadBucket", 20 | "cloudformation:DeleteStack", 21 | "cloudformation:Describe*", 22 | "cloudformation:UpdateStack" 23 | ], 24 | "Resource": [ 25 | "arn:aws:cloudformation:*:*:stack/pdfrender-*/*" 26 | ] 27 | }, 28 | { 29 | "Effect": "Allow", 30 | "Action": [ 31 | "lambda:Get*", 32 | "lambda:List*", 33 | "lambda:CreateFunction" 34 | ], 35 | "Resource": [ 36 | "*" 37 | ] 38 | }, 39 | { 40 | "Effect": "Allow", 41 | "Action": [ 42 | "s3:GetBucketLocation", 43 | "s3:CreateBucket", 44 | "s3:DeleteBucket", 45 | "s3:ListBucket", 46 | "s3:ListBucketVersions", 47 | "s3:PutAccelerateConfiguration", 48 | "s3:GetEncryptionConfiguration", 49 | "s3:PutEncryptionConfiguration" 50 | ], 51 | "Resource": [ 52 | "arn:aws:s3:::pdfrender*serverlessdeploy*" 53 | ] 54 | }, 55 | { 56 | "Effect": "Allow", 57 | "Action": [ 58 | "s3:PutObject", 59 | "s3:GetObject", 60 | "s3:DeleteObject" 61 | ], 62 | "Resource": [ 63 | "arn:aws:s3:::pdfrender*serverlessdeploy*" 64 | ] 65 | }, 66 | { 67 | "Effect": "Allow", 68 | "Action": [ 69 | "lambda:AddPermission", 70 | "lambda:CreateAlias", 71 | "lambda:DeleteFunction", 72 | "lambda:InvokeFunction", 73 | "lambda:PublishVersion", 74 | "lambda:RemovePermission", 75 | "lambda:Update*" 76 | ], 77 | "Resource": [ 78 | "arn:aws:lambda:*:*:function:pdfrender-*-*" 79 | ] 80 | }, 81 | { 82 | "Effect": "Allow", 83 | "Action": [ 84 | "apigateway:GET", 85 | "apigateway:POST", 86 | "apigateway:PUT", 87 | "apigateway:DELETE", 88 | "apigateway:PATCH" 89 | ], 90 | "Resource": [ 91 | "arn:aws:apigateway:*::/restapis*", 92 | "arn:aws:apigateway:*::/apikeys*", 93 | "arn:aws:apigateway:*::/usageplans*" 94 | ] 95 | }, 96 | { 97 | "Effect": "Allow", 98 | "Action": [ 99 | "iam:PassRole" 100 | ], 101 | "Resource": [ 102 | "arn:aws:iam::*:role/*" 103 | ] 104 | }, 105 | { 106 | "Effect": "Allow", 107 | "Action": "kinesis:*", 108 | "Resource": [ 109 | "arn:aws:kinesis:*:*:stream/pdfrender-*-*" 110 | ] 111 | }, 112 | { 113 | "Effect": "Allow", 114 | "Action": [ 115 | "iam:GetRole", 116 | "iam:CreateRole", 117 | "iam:PutRolePolicy", 118 | "iam:DeleteRolePolicy", 119 | "iam:DeleteRole" 120 | ], 121 | "Resource": [ 122 | "arn:aws:iam::*:role/pdfrender-*-*-lambdaRole" 123 | ] 124 | }, 125 | { 126 | "Effect": "Allow", 127 | "Action": "sqs:*", 128 | "Resource": [ 129 | "arn:aws:sqs:*:*:pdfrender-*-*" 130 | ] 131 | }, 132 | { 133 | "Effect": "Allow", 134 | "Action": [ 135 | "cloudwatch:GetMetricStatistics" 136 | ], 137 | "Resource": [ 138 | "*" 139 | ] 140 | }, 141 | { 142 | "Action": [ 143 | "logs:CreateLogGroup", 144 | "logs:CreateLogStream", 145 | "logs:DeleteLogGroup" 146 | ], 147 | "Resource": [ 148 | "arn:aws:logs:*:*:*" 149 | ], 150 | "Effect": "Allow" 151 | }, 152 | { 153 | "Action": [ 154 | "logs:PutLogEvents" 155 | ], 156 | "Resource": [ 157 | "arn:aws:logs:*:*:*" 158 | ], 159 | "Effect": "Allow" 160 | }, 161 | { 162 | "Effect": "Allow", 163 | "Action": [ 164 | "logs:DescribeLogStreams", 165 | "logs:DescribeLogGroups", 166 | "logs:FilterLogEvents" 167 | ], 168 | "Resource": [ 169 | "*" 170 | ] 171 | }, 172 | { 173 | "Effect": "Allow", 174 | "Action": [ 175 | "events:Put*", 176 | "events:Remove*", 177 | "events:Delete*" 178 | ], 179 | "Resource": [ 180 | "arn:aws:events:*:*:rule/pdfrender-*-*" 181 | ] 182 | }, 183 | { 184 | "Effect": "Allow", 185 | "Action": [ 186 | "events:DescribeRule" 187 | ], 188 | "Resource": [ 189 | "arn:aws:events:*:*:rule/pdfrender-*-*" 190 | ] 191 | } 192 | ] 193 | } -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: pdf-render # NOTE: update this with your service name 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs10.x 6 | 7 | plugins: 8 | - serverless-apigw-binary 9 | 10 | custom: 11 | apigwBinary: 12 | types: 13 | - '*/*' 14 | 15 | functions: 16 | hello: 17 | handler: index.main 18 | timeout: 30 19 | memorySize: 3008 20 | 21 | events: 22 | - http: 23 | path: / 24 | method: post 25 | --------------------------------------------------------------------------------