Playwright testをLambdaコンテナ上でRICを使って動作させる

## 概要

Lambda関数は通常Amazon Linux上で動作していますが、Playwrightは公式ではDebianとUbuntuしかサポートしていません（[参考](https://playwright.dev/docs/troubleshooting#linux)）。
そのため、[Lambda Runtime Interface Client(RIC)](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)を使ってDebian上のNode.jsのベースイメージを元にPlaywrightをインストールすることでLambda上で動作させました。

ローカルで動作確認したいためAWS Serverless Application Model(AWS SAM)で構築しています。

ソース類は以下にまとめています。
https://github.com/octop162/playwright-lambda

## SAM準備

`sam init` で初期化しtemplate.yamlを生成しました。
メモリ使用量を1024MB, タイムアウトを最大の900秒に設定しています。
`PackageType` を `Image` に、 `ImageUri` にECRのURIを指定することでECR上のDockerイメージを参照して実行します。

```yaml: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: 900
    MemorySize: 1024

Resources:
  LambdaPlaywright:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      ImageUri: xxxxxxxxxxxxxxxxxxxxx
    Metadata:
      DockerTag: playwright-lambda
      DockerContext: ./app
      Dockerfile: Dockerfile
```

## テストファイル準備

`example.com` にアクセスしてExampleの文字列が含まれていればテストをパスするように設定しています。

```js:tests/example.spec.mjs
import { test, expect } from "@playwright/test";

test.describe("page", () => {
  test("check", async ({ page }) => {

    await page.goto(
      "https://example.com",
    );
    await expect(page.locator('body')).toContainText("Example");
  });
});
```

### テスト実行用のLambdaハンドラを準備

直接シェルスクリプトを呼ぶのではなくNode.js上でchild_processを利用して `npx playwright test` コマンドを実行しました。
`console.log` だと標準出力をうまく表示できなかったので `console.dir` を使用していますが詳細は不明です。
また、この方法だとテスト失敗時にも正常終了してしまうのでコマンドのエラー有無を見るとより便利になりそうです。

https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback

```js:app.js
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
exports.lambdaHandler = async () => {
    try {
        const { stdout, stderr } = await exec('npx playwright test');
        console.dir(stdout);
        console.dir(stderr);
    } catch (err) {
        console.dir(err.stdout);
        console.dir(err.stderr);
        throw new Error("ERROR")
    }
};
```

## RIC準備
### package.jsonで使用するライブラリ

Playwrightの本体である `@playwright/test` と RICを導入するための `aws-lambda-ric` をpackage.jsonに追加します。

```json:package.json
{
  "name": "playwright-lambda",
  "version": "1.0.0",
  "description": "playwright-lambda",
  "main": "app.js",
  "repository": "",
  "author": "SAM CLI",
  "license": "MIT",
  "scripts": {
    "test": "npx playwright test"
  },
  "devDependencies": {
    "chai": "*",
    "mocha": "*",
    "aws-lambda-ric": "*",
    "@playwright/test": "*"
  }
}
```

### RIC用のシェルスクリプト準備

RICから呼び出されるシェルスクリプトを用意します。こちらはドキュメントをそのまま利用しています。

```sh:entry_scripts.sh
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie /usr/local/bin/npx aws-lambda-ric $@
else
  exec /usr/local/bin/npx aws-lambda-ric $@
fi 
```

### Dockerfile作成

参考に作成しました。ポイントは以下です。

* `npm install` でRICをインストール時にビルド環境が必要です
  * そのためマルチステージビルドにより事前にnode_modulesを作成してから実行用にコピーしています
* Chromiumのみインストールしています
* Debianを利用しているため、 `install-deps` でPlaywrightの実行に必要なパッケージをaptでインストールしてくれます
* RICを使用するためentry_script.shをENTRYPOINTに設定しています
* `CI=true` はPlaywrightが見ている環境変数でtrueにすることで結果を表示するWebサーバを起動する動作などが無効になります

https://github.com/aws/aws-lambda-nodejs-runtime-interface-client

```Dockerfile：Dockerfile
ARG FUNCTION_DIR="/function"

# RICインストール用
FROM public.ecr.aws/docker/library/node:14-bullseye as build-image
ARG FUNCTION_DIR
RUN apt-get update && \
    apt-get install -y \
    g++ \
    make \
    cmake \
    unzip \
    libcurl4-openssl-dev
RUN mkdir -p ${FUNCTION_DIR}
COPY package.json ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}
RUN npm install

# 実行用
FROM public.ecr.aws/docker/library/node:14-bullseye
ARG FUNCTION_DIR 
ENV CI=true
ENV PLAYWRIGHT_BROWSERS_PATH="/browser"
RUN mkdir -p /browser
WORKDIR ${FUNCTION_DIR}
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
RUN npx playwright install chromium
RUN npx playwright install-deps chromium
COPY . ${FUNCTION_DIR}
COPY ./entry_script.sh /entry_script.sh
RUN chmod 755 /entry_script.sh
ENTRYPOINT [ "/bin/bash", "/entry_script.sh" ]
CMD [ "app.lambdaHandler" ]
```

### Playwrightの設定

Playwrightの設定ファイルを用意します。 
`launchOptions` でChromiumの起動設定をしています。これらの設定をすることでLambda上でもブラウザが動作するようになるようです。
ファイル書き込み先は `/tmp` 以下に設定します。

https://github.com/JupiterOne/playwright-aws-lambda

```js:playwright.config.js
const { devices } = require("@playwright/test");
const config = {
  testDir: "./tests",
  timeout: 30 * 1000,
  expect: {
    timeout: 10000,
  },
  forbidOnly: !!process.env.CI,
  retries: 0,
  workers: 1,
  reporter: [['html', { outputFolder: '/tmp/report' }]],
  projects: [
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        launchOptions: {
          args: [
            '--autoplay-policy=user-gesture-required',
            '--disable-background-networking',
            '--disable-background-timer-throttling',
            '--disable-backgrounding-occluded-windows',
            '--disable-breakpad',
            '--disable-client-side-phishing-detection',
            '--disable-component-update',
            '--disable-default-apps',
            '--disable-dev-shm-usage',
            '--disable-domain-reliability',
            '--disable-extensions',
            '--disable-features=AudioServiceOutOfProcess',
            '--disable-hang-monitor',
            '--disable-ipc-flooding-protection',
            '--disable-notifications',
            '--disable-offer-store-unmasked-wallet-cards',
            '--disable-popup-blocking',
            '--disable-print-preview',
            '--disable-prompt-on-repost',
            '--disable-renderer-backgrounding',
            '--disable-setuid-sandbox',
            '--disable-speech-api',
            '--disable-sync',
            '--disk-cache-size=33554432',
            '--hide-scrollbars',
            '--ignore-gpu-blacklist',
            '--metrics-recording-only',
            '--mute-audio',
            '--no-default-browser-check',
            '--no-first-run',
            '--no-pings',
            '--no-sandbox',
            '--no-zygote',
            '--password-store=basic',
            '--use-gl=swiftshader',
            '--use-mock-keychain',
            '--single-process'
          ]
        }
      },
    },
  ],

  outputDir: "/tmp/test-results/",
};
module.exports = config;
```

## ローカル環境確認

ビルドを実行することでローカルのDockerがイメージを作成してくれます。

```
$ sam build
Building codeuri: /home/xxx/lambda-playwright runtime: None metadata: {'DockerTag': 'playwright-lambda', 'DockerContext': '/home/xxx/lambda-playwright/app', 'Dockerfile': 'Dockerfile'} architecture: x86_64 functions: LambdaPlaywright
Building image for LambdaPlaywright function
Setting DockerBuildArgs: {} for LambdaPlaywright function
Step 1/22 : ARG FUNCTION_DIR="/function"
Step 2/22 : FROM public.ecr.aws/docker/library/node:14-bullseye as build-image

...[省略]

 ---> xxx
Successfully built xxx
Successfully tagged lambdaplaywright:playwright-lambda


Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided

```

ビルドに成功するとローカルで動作確認できます。無事実行されテストがパスされました。

```
 sam local invoke
Invoking Container created from lambdaplaywright:playwright-lambda
Building image.................
Using local image: lambdaplaywright:rapid-x86_64.

START RequestId: 033ddf6c-d54e-4df1-b8ff-ed6ae6a21a66 Version: $LATEST
2023-XXXZ        undefined       INFO    Executing 'app.lambdaHandler' in function directory '/function'
'\nRunning 1 test using 1 worker\n·\n  1 passed (2.2s)\n'
''
END RequestId: 033ddf6c-d54e-4df1-b8ff-ed6ae6a21a66
REPORT RequestId: 033ddf6c-d54e-4df1-b8ff-ed6ae6a21a66  Init Duration: 0.19 ms  Duration: 3775.59 ms    Billed Duration: 3776 ms        Memory Size: 1024 MB    Max Memory Used: 1024 MB
null%                                        
```

## デプロイ

`--guided` オプションで指示を仰ぎデプロイを実施しました。
これによりLambda関数の作成、IAMロールの作成、ECRへのpushが自動で実施されました。

```
sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: playwright-lambda-sample
        AWS Region [ap-northeast-1]:
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: Y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [Y/n]: Y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: samconfig-test.toml
        SAM configuration environment [default]:

        Looking for resources needed for deployment:


...[省略]

Deploy this changeset? [y/N]: y

2023-03-21 19:51:38 - Waiting for stack create/update to complete

CloudFormation events from stack operations (refresh every 0.5 seconds)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                                    ResourceType                                      LogicalResourceId                                 ResourceStatusReason
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                                AWS::IAM::Role                                    LambdaPlaywrightRole                              -
CREATE_IN_PROGRESS                                AWS::IAM::Role                                    LambdaPlaywrightRole                              Resource creation Initiated
CREATE_COMPLETE                                   AWS::IAM::Role                                    LambdaPlaywrightRole                              -
CREATE_IN_PROGRESS                                AWS::Lambda::Function                             LambdaPlaywright                                  -
CREATE_IN_PROGRESS                                AWS::Lambda::Function                             LambdaPlaywright                                  Resource creation Initiated
CREATE_COMPLETE                                   AWS::Lambda::Function                             LambdaPlaywright                                  -
CREATE_COMPLETE                                   AWS::CloudFormation::Stack                        playwright-lambda-sample                          -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - playwright-lambda-sample in ap-northeast-1
```


## 実環境での確認
実際のLambdaを実行させてみました。画面確認だけのテストですが6000msほど動作時間かかっていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359375/c7a6e94f-38a1-8cc8-35e9-9a6b0babbbad.png)

ちなみにECRのイメージサイズですが700MBちかく消費しています。
Debian-slimだとインストールがうまくいかずDebianを使用したので重めです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359375/b6c6e8fb-4938-badd-0c4b-62ecddf9e266.png)

## 感想
* CodeBuildやgithub actionsなら簡単に結果レポートを保存できるなど利点があります
* Node.jsのRICは14系までのサポートのためそろそろサポートが切れそうです
* Lambdaは無料枠が多いので操作が必要であるような少し複雑なページ監視にも利用できます
* ECRの保持料金が少しかかるので無料枠の500MBになるようサイズを圧縮したいです