AWS Lambdaでpuppeteerを動かしスクリーンショットをs3に保存する

AWS Toolkit for Visual Studio Codeがリリースされたのでserverlessなアプリを作りながら試してみることに。

前から気になっていたHeadless Chromeをjsから操作できるpuppeteerを使って、スクリーンショットをs3に保存するAPIを作ることにした。

「Create new SAM Application」でLambdaアプリの雛形を作り、VS Code上からLambdaのデプロイができた。

とはいえ毎回デプロイ対象の指定が必要になるので、ターミナルから実行したほうが楽だと感じた。

実装

先人の記事を参考に、puppeteer用のLambda Layerを追加し、雛形のtemplateを修正。

動かすのを優先で今回は雛形のままHelloWorldFunctionというキーにしてしまった。

# template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  LambdaLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: aws-lambda-puppeteer
      CompatibleRuntimes:
        - nodejs8.10
      ContentUri: lambda-layer.zip

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      Policies:
        - S3CrudPolicy:
            BucketName: {s3_bucket_name}
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: nodejs8.10
      Layers:
        - !Ref LambdaLayer
      MemorySize: 512
      Timeout: 120
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          PARAM1: VALUE
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /
            Method: get

Outputs:
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"

  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn

  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

lambdaのコードは以下。

// src/app.js 

process.env["HOME"] = process.env["LAMBDA_TASK_ROOT"];
const chromium = require("chrome-aws-lambda");
const puppeteer = require("puppeteer-core");
const aws = require("aws-sdk");
const crypto = require("crypto");
const s3 = new aws.S3();
const bucket = "s3_bucket_name";

const generateHash = data => {
  const sha256 = crypto.createHash("sha256");
  sha256.update(data);
  return sha256.digest("hex") + ".jpg";
};

exports.lambda_handler = async (event, context) => {
  const query = event.queryStringParameters;
  const url = query && query.url ? query.url : "https://www.amazon.co.jp/";
  let result = null;
  let browser = null;

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

    let page = await browser.newPage();
    await page.goto(url);
    const screenshot = await page.screenshot({ fullPage: true, type: "jpeg" });
    const fileName = generateHash(url);
    await s3
      .putObject({
        Bucket: bucket,
        Key: fileName,
        Body: screenshot
      })
      .promise();

    const download_url = s3.getSignedUrl("getObject", {
      Bucket: bucket,
      Key: fileName,
      Expires: 86400
    });
    result = { download_url: download_url };
  } catch (error) {
    return context.fail(error);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }

  return context.succeed({
    statusCode: 200,
    body: JSON.stringify(result)
  });
};

日本語が文字化けするので対応方法にハマったが、下記の記事の方法で手軽に対応できた。

AWS Lambda + @serverless-chrome/lambda + Puppeteer 環境で任意のフォントを使えるようにする - Qiita

.fontsディレクトリにfontファイルを設置し、環境変数HOMEを設定する。

src
├── .fonts
|          └── NotoSansCJKjp-Regular.otf
├── app.js

デプロイ用にシェルスクリプトを追加。

# deploy.sh
sam package \
    --template-file template.yaml \
    --output-template-file packaged.yaml \
    --s3-bucket sam-package-bucket

sam deploy \
    --template-file packaged.yaml \
    --stack-name puppeteer \
    --capabilities CAPABILITY_IAM

template.yamlでAPIキーを設定するのが手間だったので、今回はAPI Gatewayのダッシュボードから手動で設定した。

これで任意のURLに対してスクリーンショットを撮れるAPIができた。

以下のようにAPIキーを指定してcurlでリクエストを送るとスクリーンショットのURLが返ってくる。

curl 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod?url=https://www.amazon.co.jp/' --header 'x-api-key:API_KEY'

{"download_url":"https://~"}

追記

Expiresの指定より前にトークンが期限切れになってしまう問題があったので、専用のIAMユーザを利用する方法に変更した。

s3の署名付きURLが有効期限より前に見れなくなってしまう

参考