Puppeteer on Lambda (Node.js 12.x) で日本語ページのキャプチャを取る方法 簡単

## やりたいこと
Lambda上でPuppeteerを動かしてキャプチャを取るとき、日本語フォントを正しく表示させる。
（デフォルトではLambdaに日本語フォントが入っていないため、何もしないと日本語がすべて豆腐になってしまうのだ。）

## 環境
* AWS Lambda
    * Node.js 12.x
* ローカル
    * Windows10
    * Node.js v12.16.2
    * npm 6.14.4

## やり方（ざっくり）
* Lambda Layerに日本語フォントをzip化してアップロードする。
* Lambda関数にそのLayerを追加する。
* Lambda関数内でLayerのフォントファイルを参照できるようにindex.jsで`process.env['HOME'] = "/opt";`を定義する。

## やり方（詳細）

```shell:ローカルのディレクトリ構成
puppeteer_sample
├─ modules # Layerに登録するnpmモジュール群
│   ├─ node_modules
│   └─ package-lock.json
├─ .fonts  # Layerに登録する日本語フォント置き場
│   ├─ ipag.ttf
│   ├─ ipagp.ttf
│   ├─ NotoSansCJKjp-Bold.otf
│   └─ NotoSansCJKjp-Regular.otf
└─ lambda  # Lambda本体のコード群
    └─ index.js
```
### Lambda上でpuppeteerを動作させる
[こちらの記事](https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d)が非常に分かりやすく、参考になりました。
参考記事は、LambdaのランタイムにNode.js 8.10を選択できる時代の記事ですが、Node.js 12.xと読み替えても通用します。

参考記事とほぼ同じですが、僕のやった手順を簡単にメモしておきます。
### Lambda Layerに登録するモジュールの作成

```shell:cmd
$ cd modules
$ npm i chrome-aws-lambda puppeteer-core
```

### modulesを丸ごとzipしてmodules.zipを作成
![コメント 2020-04-26 233309.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/d038fe11-d365-20e5-7cf6-2e5817939d2f.png)

### Lambda Layer作成
このステップはほぼ[参考記事](https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d)のまんまです。

###### AWSコンソールからLambda Layerを開く
![コメント 2020-04-26 233955.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/0a1e6f1f-e5a4-f72e-a3a8-ae4d81def6c1.png)

###### レイヤー作成（npmモジュール群）
![コメント 2020-04-26 233955-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/002b61d7-ed1f-2b7a-85e0-890165ddbf0d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/0f7ef11d-669d-4e78-702f-6edc1bc2d088.png)

* 名前：任意
* .zipファイルをアップロード：先ほど作成したmodules.zipを選択
* 互換性のあるランタイム：Node.js 12.x
* 作成ボタン押下

### 日本語フォントを登録する
Lambda Layerに登録するための日本語フォントをローカルにダウンロードします。
僕の場合は、以前取得していた日本語フォントファイルが手元にあったので、詳細な手順は割愛します。
[IPAのサイト](https://ipafont.ipa.go.jp/old/ipafont/download.html)からダウンロードしたり、[適当な記事](https://qiita.com/nowri/items/1c69b9b25f2958bd9f97)を参考に各自調達してください。

取得したフォントファイルを.fonts配下に格納し、エクスプローラから.fontsフォルダごとzipして.fonts.zipを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/7fd0051e-f08d-78bf-64fb-0167e65311de.png)


###### レイヤー作成（日本語フォント）
再びAWSコンソールからレイヤー作成を行います。
![コメント 2020-04-26 233955-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/002b61d7-ed1f-2b7a-85e0-890165ddbf0d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/b7efcbd1-ef3a-c531-4bd8-be97feda6ca2.png)

* 名前：任意
* zipファイルをアップロード：先ほど作成した.fonts.zipを選択
* 互換性のあるランタイム：Node.js 12.x

### Lambda関数作成
このステップもほぼ[参考記事](https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d)のまんまです。

###### AWSコンソールからLambda 関数を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/08f964e8-1b39-4f6b-afde-4e04f8b6b1aa.png)

###### 関数作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/4b181c33-bf52-f52a-8dd2-d6273607d406.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/89d2e028-c00b-265f-83ba-a36e8e3a6b8c.png)

* 関数名：任意
* ランタイム：Node.js 12.x
* 実行ロール：後で編集するので、作成時は適当に。
* 「関数の作成」を押下

###### レイヤー登録
デザイナービューでLayersを選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/8ea89097-eda9-cc83-b0e1-81b08423b7d1.png)

「レイヤーの追加」ボタンを押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/2a4c9d13-7c0c-4a12-a5cf-d647ac03f381.png)

レイヤーを追加する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/62648cd8-0735-5c99-6b14-9be8710449eb.png)

追加したいレイヤーとバージョンを選択し、「追加」ボタンを押下する。
この手順を繰り返して、先ほど作成した2つのレイヤーを追加する。


###### index.jsの編集
デザイナービューでLambda関数を選択し、メイン処理を記述する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/f7f916db-7969-c43d-17cd-c6fbacf35791.png)

```javascript:index.js
/* 日本語フォントが入っている.fontsを読み込ませるためにHOMEを設定する */
process.env['HOME'] = "/opt"; // Layerの内容は/optにある

const AWS = require('aws-sdk');
const chromium = require('chrome-aws-lambda');
const puppeteer = require('puppeteer-core');

// パラメータ定義
const SAVE_BUCKET_NAME = 'キャプチャ保存先のバケット名'
const operations = [
  { method: 'goto', args: ['https://www.yahoo.co.jp/'] },
  { method: 'waitFor', args: [1000] }
]

/* ユーティリティ関数 */
// operationsに従ってブラウザ操作。clickやwaitFor時にキャプチャを取得
const execOperations = async function (page, operations, result, jpgBuf) {
  for (const op of operations) {
    console.log(`${op.method} (${op.args[0]} ${op.args.length > 1 ? " ," + op.args[1] : ""})`)
    if (op.method === 'click') {
      jpgBuf.push(await page.screenshot({ fullPage: true, type: 'jpeg' }));
    }
    await page[op.method](...op.args)
    if (op.method === 'waitFor') {
      jpgBuf.push(await page.screenshot({ fullPage: true, type: 'jpeg' }));
    }
  }
}

// S3にキャプチャを保存
const saveJpg = async (jpgBuf) => {
  const s3 = new AWS.S3();
  const now = new Date();
  now.setHours(now.getHours() + 9);
  const nowYMD = now.getFullYear() +
    (now.getMonth() + 1 + '').padStart(2, '0') +
    (now.getDate() + '').padStart(2, '0')
  const nowHMS = (now.getHours() + '').padStart(2, '0') +
    (now.getMinutes() + '').padStart(2, '0') +
    (now.getSeconds() + '').padStart(2, '0');

  for (const idx in jpgBuf) {
    const fileName = 'screenshots/' + nowYMD + '/' + nowHMS + '_' + ('0000' + idx).slice(-4) + '.jpg';
    let s3Param = {
      Bucket: SAVE_BUCKET_NAME,
      Key: fileName,
      Body: jpgBuf[idx]
    };
    await s3.putObject(s3Param).promise();
  }
}

// メイン処理
exports.handler = async (event, context) => {
  let browser = null;
  const result = {}
  const jpgBuf = []

  try {
    // 初期処理
    browser = await puppeteer.launch({
      args: chromium.args.concat(['--lang=ja']),
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
    });

    let page = await browser.newPage();
    await page.setExtraHTTPHeaders({
      'Accept-Language': 'ja-JP'
    });

    // ブラウザ操作
    await execOperations(page, operations, result, jpgBuf)

    // 取得したキャプチャをS3に保存
    await saveJpg(jpgBuf)

  } catch (error) {
    return context.fail(error);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }

  return context.succeed(result);
};
```

###### Lambdaの設定を編集する
メモリとタイムアウトはデフォルトだと少なすぎる／短すぎるので、適当に増やす。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/c3703fa9-24b6-67e7-b509-ce79febe3fb7.png)

###### Lambda関数にアタッチされているIAMロールにS3書き込み権限を付与する
Lambda関数の基本設定の「編集」ボタンを押下してLambdaにアタッチされているIAMロールを確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/6548787f-dced-0d2e-e4d2-6fff5af639c1.png)
「IAMコンソールでxxxxxロールを表示します」のリンクからIAMロールを編集し、S3書き込み権限を付与する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/09ea6ac8-99c0-25ba-8914-81d5bcd5d2c2.png)



###### S3バケットを作成する
キャプチャ保存用のS3バケットを作成する。
Lambda関数のindex.js の SAVE_BUCKET_NAME に作成したバケット名を設定する。

### Lambda関数の実行
Lambda関数の「テスト」を押下する。（初回はテストオブジェクトの定義が必要だが、適当でOK。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/28f6babd-5321-a941-bb04-64d1a89db55b.png)

###### 結果
日本語を含むページのキャプチャが文字化けすることなく取得できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/179eb44f-591b-b4df-9cf9-e44f6d0feeae.png)