Puppteerでキャプチャ画像を取得 part1

## 概要
エンジニアもどき2年目に入りました。色々あって以下を行うLambda関数を作成することになりました。
色々学びがあったので備忘録がわり兼アウトプットのために書くことにしました。

1. 特定のページのキャプチャ取得
2. 取得したキャプチャをS3に保存
3. S3に保存したURLをDyanmoに記録

part1では`1. 特定のページのキャプチャ取得`の実装とデプロイまで行います。
part1での実装内容は[こちら](https://github.com/syu-y/puppeteer-capture/tree/part1)

## 前提
以下の内容が出てきます。インストールなどの準備は完了している前提です。

- nodejs
- ServerlessFramework
- yarn
- webpack
- iamのユーザ作成済み

## 実装作業
### 1. 準備
フォルダとテンプレートの作成をします。

```zsh
mkdir puppeteer-capture
cd puppeteer-capture
serverless create --template aws-nodejs
```

作成されたテンプレートは次のような構成になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/8f311725-c953-15be-a6f3-28c51b76cc26.png)

また、webpackを使用する（[modeule構文など](https://www.wakuwakubank.com/posts/466-javascript-module-import-export/)esnextで実装する）ため次のパッケージも追加します。

```zsh
yarn add -D webpack webpack-cli
yarn add -D @babel/core @babel/preset-env babel-loader
yarn add -D serverless-webpack
```

webpackの設定を書きます。

```json:jsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "baseUrl": ".",
  },
  "exclude": ["node_modules"]
}
```

```js:webpack.config.js
const path = require('path');
const slsw = require('serverless-webpack');

module.exports = {
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: slsw.lib.webpack.isLocal
    ? 'cheap-module-eval-source-map'
    : 'source-map',
  output: {
    libraryTarget: 'commonjs',
    filename: '[name].js',
    path: path.join(__dirname, '.webpack'),
  },
  target: 'node',
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/,
        include: __dirname,
        use: [
          {
            loader: 'babel-loader',
          },
        ],
      },
    ],
  },
};

```

以上で事前準備は完了です。

### 2. キャプチャを取得する関数の実装
キャプチャを取得してローカルに保存する関数を作成します。

#### 2-1. ライブラリの追加
今回はブラウザを操作する`puppeteer-core`とAWS上でchromium（ブラウザ）を動かす`chrome-aws-lambda`を使用します。また、ローカルでの動作確認用にpuppeteerも追加しています。

```zsh
# ライブラリの追加
yarn add puppeteer-core chrome-aws-lambda
yarn add -D puppeteer
```

事前準備で追加したwebpackの設定を除き、`package.json`に以下の3つのライブラリが追加されていれば完了です。


```json:package.json
{
  "devDependencies": {
    // ・・・　省略　・・・
    "puppeteer": "^3.0.2",
    // ・・・　省略　・・・
  },
  "dependencies": {
    "chrome-aws-lambda": "^2.1.1",
    "puppeteer-core": "^3.0.2"
  }
}
```

#### 2-2. 実装
`handler.js`にキャプチャの取得/ローカルへの保存を行う関数を実装します。

```js:handler.js
import { args, defaultViewport, executablePath, headless, puppeteer } from 'chrome-aws-lambda';
import { writeFileSync } from 'fs';

const getCapture = async (url) => {
  // ブラウザの起動
  let browser = null;
  try {
    browser = await puppeteer.launch({
      args,
      defaultViewport,
      executablePath: await executablePath,
      headless,
    });

    // ページに移動
    const page = await browser.newPage();
    await page.goto(url);

    // キャプチャの取得（フルページ、jpegを指定）
    return await page.screenshot({ fullPage: true, type: 'jpeg' });
  } catch (error) {
    console.log(error);
    return null;
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }
};

export const captureFunction = async event => {
  const url = event.url || 'https://google.com/';

  // キャプチャ取得
  const jpgBuf = await getCapture(url);
  if (!jpgBuf) {
    return { statusCode: 500, body: 'キャプチャの取得に失敗しました.' };
  }

  // ファイルに書き出し
  writeFileSync('/tmp/hoge.jpg', jpgBuf);

  return { statusCode: 200, body: 'キャプチャの取得に成功しました。' };

};
```

#### 2-3. 動作の確認
まずは`serverless.yml`を以下のように編集します。
regionや[profile](https://qiita.com/ozaki25/items/883aec0ab4398aa4abbe#aws%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E8%A8%AD%E5%AE%9A)の項目は各自の環境に合わせて記述してください。

```yml:serverless.yml
service: puppeteer-capture
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  profile: private
  stage: dev
# 使用するプラグイン
plugins:
  - serverless-webpack
# 関数の設定
functions:
  captureFunction:
    handler: handler.captureFunction
```

以下を実行して動作の確認を行います。

```zsh
# ローカルでの動作確認
sls invoke local --function hello -c ./serverless.yml
```

一瞬ブラウザが立ち上がり`/tmp`に`hoge.jpg`という名前でキャプチャ画像が取得できていればOKです。
任意のページのキャプチャを取得する場合は、以下のように実行します。

```zsh
# 任意のページのキャプチャを取得
sls invoke local --function captureFunction --data '{"url":"https://qiita.com/"}' -c ./serverless.yml
```

## デプロイ
作成したLambda関数をAWS上にデプロイします。

### 1. 準備
`chrome-aws-lambda`込みでlambdaにデプロイすると容量が結構大きくなってしまうためパッケージはLambda Layerに入れておき、そこから使うことにします。

まずは、Layersに配置するものを格納しておくディレクトリを作成し、パッケージを追加します。
[ドキュメント](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path)によると`[任意の名前]/nodejs/node_modules`にパッケージを追加する必要があるようです。

```zsh
# ディレクトリの作成
mkdir layers/nodejs
cd layers/nodejs
# パッケージの追加
yarn init -y
yarn add chrome-aws-lambda puppeteer-core
```

あわせて、`webpack.config.js`に外部依存の設定を追加します。

```js:webpack.config.js
  //　・・・　省略　・・・
  target: 'node',
  // 追加
  externals: ['chrome-aws-lambda'],
  //　・・・　省略　・・・
};
```

### 2. Layersの設定

Layersに格納するパッケージを格納しているパスと関数からの参照を行うための設定を`serverless.yml`に記述します。
コンソールからもLayersの設定はできますが、今回は設定に関しては全てコンソールを使わずに行ないます。
Layer設定の`name`項目によりlambdaだけでなくlayerもstageでの切り分けができます。

```yml:serverless.yml
service: puppeteer-capture
provider:
  # ・・・　省略　・・・
  # 追加1 : Layer名などの環境変数
  environment:
    STAGE: ${self:provider.stage}
    PREFIX: ${self:service}-${self:provider.stage}
    CAPTURE_LAYER: ${self:provider.environment.PREFIX}-capture-layer
# ・・・　省略　・・・
# 関数の設定
functions:
  captureFunction:
    handler: handler.captureFunction
    # 追加2 : Layerの参照
    layers:
      - { Ref: CaptureLayerLambdaLayer }
# 追加3 : Layerの設定
layers:
  CaptureLayer:
    path: layers
    name: ${self:provider.environment.CAPTURE_LAYER}
```

### 3. デプロイと動作確認

以下のコマンドでデプロイすると画像のような結果が得られます。

```zsh
# デプロイ
serverless deploy       
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/08c4a2ea-0cd7-41b1-eea2-044beac5de35.png)

後はデプロイした関数の動作を確認するだけです。

```zsh
# デプロイした関数の実行
sls invoke --function captureFunction -c ./serverless.yml   
sls invoke --function captureFunction --data '{"url":"https://qiita.com/"}' -c ./serverless.yml
```

以下のようにレスポンスが返ってきていれば完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/1f20102d-40ff-3f68-df95-445dbd67ef4e.png)

lambdaは実行後、使用されたすべてのリソース（に格納されているすべてのファイルを含む）が破棄されてしまうので、`/tmp`ではなくS3などに保存する必要があります。そこで次回は`2. 取得したキャプチャをS3に保存`するように修正します。

## おわりに

特に大変だったのは次の3点でした。

- ちゃんとwebpackを使う
 - パス周りの設定方法（相対パス排除）[webpack, Jest, Flowのimportの相対パスを綺麗にする](https://qiita.com/mwatanabe@github/items/a3db438863d56fb86fb5) 
- ServerlessFrameworkだけで完結
 - コンソールでLayerのzip追加しなくても何とかできないか
- Layerをstageで切り分ける

AWS Serverless Application Modelも気になるのでいずれはそちらも使ってみたいなぁ。

## 参考

- https://qiita.com/ozaki25/items/883aec0ab4398aa4abbe
- https://qiita.com/ayu_kane/items/6b4a0c139e34a31051dd
- https://qiita.com/t-yasukawa/items/8003e4aeaab131882b92#%E5%8F%82%E8%80%83