Docker で Puppeteer を動かす

# 動機
CIを回していると、定期的にデプロイされた画面を確認してスモークテストを行い結果を保存しておいたいということがあります。
そこで Docker ベースで Puppetter を実行できるようにしました。
実行時に作成されるスクリーンショットはホスト側にマウントしたディレクトリに作成されるようにします。
この時、Puppeteer の [Running Puppeteer in Docker](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker) だとハマるポイントがあったので、そのメモです。

# 結論
- Running Puppeteer in Docker のやり方（pptruser で実行）だと、screenshot を撮るときにパーミッションのエラーがでる。マウントするディレクトリを事前に作成することでエラーを回避できる
- デフォルトの root ユーザで実行する場合は、screenshot を撮るときにエラーが出ない。ただし、`--no-sandbox` オプションを利用する必要がある

# 利用バージョン
- Docker: 20.10
- Node: 12.22.1
- google-chrome: 90.0.4430.93
- Puppeteer: 9.0.0

# 流れ
1. Puppeteer の Docker image の作成
1. 作成したイメージを利用して Puppeteer を実行する

# ドキュメントの方法（pptruser）で実行する場合
## 1. Docker image の作成

[Running Puppeteer in Docker](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker) に従って、Dockerfile を作成します。

```Dockerfile
FROM node:12-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# Install puppeteer so it's available in the container.
RUN npm i puppeteer \
# Add user so we don't need --no-sandbox.
# same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules

# Run everything after as non-privileged user.
USER pptruser

CMD ["google-chrome-stable"]
```

## 2. Puppeteer のスクリプトの作成

```example.js

const puppeteer = require('puppeteer')
const screenshot = 'example.png';

(async () => {
  const browser = await puppeteer.launch({
    headless: true,
    args: [
      '--disable-dev-shm-usage'
    ]
  })

  try {
    const page = await browser.newPage()
    await page.goto('https://www.amazon.co.jp/')
    await page.type('#twotabsearchtextbox', 'nyan cat')
    await page.click('#nav-search-submit-button')
    await page.waitForNavigation()
    await page.screenshot({ path: screenshot, fullPage: true })
    console.log('See screenshot: ' + screenshot)
  } catch (e) {
    console.error(e)
  } finally {
    browser.close()
  }
})()
```

## 3. 実行する
以下のコマンドを実行することで、`./screenshot/` 以下にファイルが作成されます。

```
docker build -t puppeteer-chrome-linux-pptr .

mkdir -p screenshot
chmod 777 screenshot

docker run -i --init --rm --cap-add=SYS_ADMIN \
  --shm-size=256m \
  -w /screenshot/ \
  -v "$(pwd)/screenshot:/screenshot" \
  --name puppeteer-chrome puppeteer-chrome-linux-pptr \
  node -e "`cat example.js`"
```

```
mkdir tmp
chmod 777 tmp
```
がないと、page.screenshot() の呼び出し時に以下のエラーで怒られます。

```
[Error: EACCES: permission denied, open '/screenshot/example.png'] {
  errno: -13,
  code: 'EACCES',
  syscall: 'open',
  path: '/screenshot/example.png'
}
```
マウントするディレクトリが存在しない場合、Docker はディレクトリを作成してくれます。しかし、作成されるディレクトリは root が所有者で 755 が権限になっています。
コンテナ内の pptruser では書き込むことができないため、エラーになります。


# root ユーザで実行する場合
## 1. Docker image の作成

pptruser 周りの設定は消して、Dockerfile を作成します。

```Dockerfile
FROM node:12-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# Install puppeteer so it's available in the container.
RUN npm i puppeteer

CMD ["google-chrome-stable"]
```

イメージをビルドします。

```
docker build -t puppeteer-chrome-linux .
```

## 2. Puppeteer のスクリプトの作成

```example.js

const puppeteer = require('puppeteer')
const screenshot = 'example.png';

(async () => {
  const browser = await puppeteer.launch({
    headless: true,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage'
    ]
  })

  try {
    const page = await browser.newPage()
    await page.goto('https://www.amazon.co.jp/')
    await page.type('#twotabsearchtextbox', 'nyan cat')
    await page.click('#nav-search-submit-button')
    await page.waitForNavigation()
    await page.screenshot({ path: screenshot, fullPage: true })
    console.log('See screenshot: ' + screenshot)
  } catch (e) {
    console.error(e)
  } finally {
    browser.close()
  }
})()
```

## 3. 実行する
以下のコマンドを実行することで、`./screenshot/` 以下にファイルが作成されます。

```example.sh
docker build -t puppeteer-chrome-linux-root .

docker run -i --init --rm --cap-add=SYS_ADMIN \
  --shm-size=256m \
  -v "$(pwd)/screenshot:/screenshot" \
  -w /screenshot/ \
  --name puppeteer-chrome puppeteer-chrome-linux-root \
  node -e "`cat example.js`"
```

# 備考
## `--no-sandbox` を利用することについて
Chrome(chromium) にはでホストを守るための Sandbox 機能があります。これは信頼できないサイトなどを閲覧するときにホストに影響が出ないようにするための機能です。今回はテスト環境へのアクセスを伴うテストのため、 `--no-sandbox` 利用でも問題ないといえます。

## screenshot ディレクトリに 777 を与えることについて
もっと大切なデータなどはこのような簡易的な方法ではなく、ほかの記事にあるような uid、gid をそろえる方法などを取るのがよいと思います。今回は、簡便に取得したかったため、この方法を取りました。

# 雑感
コンテナベースにすることで、手軽にCI で利用できるようなり、簡単なテストを定期的に実行できるようになりました。
また、AWS CloudWatch synthetics では、Puppeteer をラップしたスクリプトを実行できるようなので、そちらも試してみたいなと思いました。 

# 参考
- https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker
- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/sandboxing.md
- https://peter.sh/experiments/chromium-command-line-switches/#no-sandbox-and-elevated
- https://docs.docker.com/storage/bind-mounts/