コンテナデプロイLambdaで Puppeteerを用いて PDFを作成する

C-limber(クライマー)株式会社 エンジニアの柿添です。

新たな2022年となりました。
忙しさが加速しておりますが、ありがたい限りです。
本年もどうぞよろしくお願いいたします。

前回 Puppeteerについて記事を書きました。
実際に使用したケースをみていただいた方がより有用性にご理解いただけるかと思います。

そこで今回はPuppeteerをコンテナ型Lambdaとして稼働させ、PDFを作成したいと思います。

AWS Lambda とは

AWS LambdaはAWSのサービスであり、
「サーバレスでコードを実行できるコンピューティングサービス」です。

  • クラウド上にコードを設置しておいて実行することができる
  • Java、Go、PowerShell、Node.js、C#、Python、Ruby のコードをサポート
  • サーバやミドルウェアの管理は全てAWSがやってくれる
  • (サーバ周りの管理が要りません!)
  • 実行時のトリガーを柔軟に指定可能

等々エンジニアにとって嬉しいことばかりです。
類似のサービスではGCPの「Cloud Functions」などがあります。

サーバ負荷の高い画像処理や、データが更新された際の集計処理などを外出しして、サーバ負荷を抑えることも可能です。
本体サーバを守るため、処理の一部を切り出して有効活用していきたいですね。

AWS Lambda のトリガー

AWS側のトリガー

  • API Gateway
  • AWS IoT
  • Alexa Skills Kit
  • Alexa Smart Home
  • Apache Kafka
  • Application Load Balancer
  • CloudFront
  • CloudWatch Logs
  • CodeCommit
  • Gognito Sync Trigger
  • DynamoDB
  • EventBridge (CloudWatch Events)
  • Kinesis
  • MQ
  • MSK
  • S3
  • SNS
  • SQS

パートナーイベントソース(Amazon EventBridge を使用)トリガー

  • Atlassian – Ogsgenie
  • Auth0
  • BUIDLhub
  • Blitline
  • Buildkite
  • CleverTap
  • Datadog
  • Epsagon
  • Freshworks
  • Game Server Services Co., Ltd.
  • Kloudless
  • Mackerel
  • MongoDB
  • New Relic
  • OneLogin
  • PLAID, Inc.
  • PagerDuty
  • Payshield
  • Saviynt
  • Segment
  • Shopify
  • SignalFx
  • Site24x7
  • SugarCRM
  • Symantec
  • Thundra
  • Whispir
  • Zendesk

(2022/01/12現在)

DatadogやSite24x7、Shopifyなんかもトリガー可能です!
非常に頼もしいですね。

AWS Lambda 4つのデプロイ方法

AWS Lambda のデプロイ方法は大きく4つの方法に分けられます。

  • AWSマネジメントコンソールからLambdaのソースコードを編集
  • ローカルで開発したソースコードをZIP化しアップロード
  • S3に置いてあるZIP化されたソースコードをデプロイ
  • ECRに配置したコンテナイメージをデプロイ

今回は4番目のECRに配置したコンテナイメージをデプロイしてみます。

デプロイパッケージの割り当てサイズ制限

  • zip形式で直接アップロードする場合は50MB
  • 非圧縮パッケージの場合は250MB

※ レスポンスは6MBの制限があります

Lambda コンテナデプロイの強み

コンテナイメージのサイズ上限は10GBと通常の割り当てパッケージサイズのおよそ40倍!
→ 巨大なライブラリや機械学習モデルを利用できます!

準拠すべきルールとしては以下のようなものがあります。

  • Linuxベースのコンテナイメージであること
  • コード実行に必要な全てのファイルが読み取り可能なこと
  • Lambda実行環境は基本的にRead-Only
  • 書込み可能なのは/tmpディレクトリのみ
  • 書込み可能なサイズは最大512MB

コンテナイメージに複数機能を仕込むかどうか

  • 1つのコンテナイメージを複数のLambda関数で使いまわす
  • Lambda関数毎にCMDの指定を変える等する
  • 1つのコンテナイメージにまとめてしまう場合、疎結合に保てなくなる
  • → 項目や機能、役割・責任毎にバランスを考えて検討する必要がある

補足 Amazon ECR とは

Amazon ECR とは Elastic Container Registry の略で、
「コンテナソフトウェアをどこにでも簡単に保存、共有、デプロイできるサービス」です。

https://aws.amazon.com/jp/ecr/

  • Docker コンテナイメージを配置するレジストリ
  • Docker コンテナイメージを保存・管理・デプロイが容易に可能
  • ECS, EKS, Fargateにも統合されている

補足 ECS, EKS, Fargate

ECS: Amazon Elastic Container Service

  • Amazon EC2インスタンスを用いたDockerコンテナを管理するサービス
  • コンテナ起動方法は「EC2」と「Fargate」の2つがある

EKS: Amazon Elastic Kubernetes Service

  • Kubernetes のマネージドサービス
  • オープンソースの Kubernetes が殆どそのまま提供

Fargate: AWS Fargate

  • OS・ミドルウェアの構築を必要としない
  • インスタンスタイプの管理不要
  • クラスター管理不要
  • オートスケーリング
  • 注意: 固定パブリックIPの割り当て不可
  • 注意: sshが使えない
  • 注意: docker execが使えない

ローカルでコンテナ・Lambdaの構築

まず初めにローカル環境でコンテナの構築、Lambdaの記述を行いたいと思います。

ディレクトリ構造と設置ファイル

$ mkdir aws-lambda-puppeteer-pdf

まず初めにディレクトリを設置。

.
├── node_modules
│
├── .dockerignore
├── .env
├── .gitignore
├── app.js
├── docker-compose.yaml
├── Dockerfile
├── package.json
└── package-lock.json
                            

ディレクトリ内の構成は上記のようにしました。

.gitignoreは余計なファイルをgit管理下へ置かないための設置。

■ コンテナ周り
.envは環境変数のため設置(今回は未使用)。
.dockerignoreは余計なファイルをdockerデーモンに転送しないよう設置。
docker-composeはローカル環境で検証しやすくするために設置。
Dockerfileは今回のコンテナの本体情報です。

■ Lambda周り
app.js が今回のLambdaの本体で Node.js で記述、
node_modules, package*.jsonは nodeライブラリ用に設置。

コンテナ情報

コンテナは Lambda コンテナイメージのランタイムサポート からNode.jsのイメージ(public.ecr.aws/lambda/nodejs:14)を利用しました。

Dockerfile

.FROM public.ecr.aws/lambda/nodejs:14

ARG LAMBDA_TASK_ROOT="/var/task"

RUN yum install -y \
    libX11 \
    libXcomposite \
    libXcursor \
    libXdamage \
    libXext \
    libXi \
    libXtst \
    cups-libs \
    libXScrnSaver \
    libXrandr \
    alsa-lib \
    pango \
    atk \
    at-spi2-atk \
    gtk3 \
    google-noto-sans-japanese-fonts

COPY ./package.json ${LAMBDA_TASK_ROOT}
COPY ./package-lock.json ${LAMBDA_TASK_ROOT}
RUN npm install

COPY ./app.js ${LAMBDA_TASK_ROOT}

CMD ["app.handler"]

yum install でpuppeteerに必要なライブラリをインストールしておきます。
Puppeteer PDF 出力時に日本語が豆腐にならないように、日本語フォントもインストール。
package*.json をコピー後 npm install で Node.js側のライブラリもインストールしておきます。

package*.json

コンテナ型Lambdaであるためパッケージのサイズは気にせず、純粋なPuppeteerのみ利用するよう記述しています。
ただただスクレイピングするだけにPuppeteerを使うのであればさほど問題にはなりませんが、
スクリーンショットを撮ったり、PDFを生成したりする場合は話が違ってきます。 他のヘッドレスクロムや軽量化されたライブラリではすぐに豆腐化(文字化け)する問題も出てくるため、 純粋なPuppeteerを利用しています。
package.json
package-lock.json

docker-compose.yaml

version: "3.6"

services:
  puppeteer-pdf:
    container_name: puppeteer-pdf
    build: .
    volumes:
      - $HOME/.aws/:/root/.aws/
      - ./output/:/var/task/output/
    ports:
      - "9000:8080"
    env_file:
      - .env

検証中に楽するために書きました。

app.js

const getPuppeteer = async () => {
    const puppeteer = require('puppeteer');
    return await puppeteer.launch({
        headless: true,
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '-–disable-dev-shm-usage',
            '--disable-gpu',
            '--no-first-run',
            '--no-zygote',
            '--single-process',
        ]
    });
};

exports.handler = async (event, context) => {

    const url = 'queryStringParameters' in event && 'url' in event.queryStringParameters ? event.queryStringParameters.url : 'https://www.yahoo.co.jp/';
    const width = 'queryStringParameters' in event && 'width' in event.queryStringParameters ? event.queryStringParameters.width : 1680;
    const height = 'queryStringParameters' in event && 'height' in event.queryStringParameters ? event.queryStringParameters.height : 1050;

    const browser = await getPuppeteer();
    const page = await browser.newPage();
    await page.setViewport({
        width: width,
        height: height,
    });
    await page.goto(url, {
        waitUntil: 'networkidle0',
    });

    const result = await page.title();
    const pdf = await page.pdf({
        // path: 'output/output.pdf', // PDFファイルの出力パス(ローカルテスト用)
        scale: 1,                     // 拡大縮小率 1=100%
        displayHeaderFooter: false,   // header,footer 表示
        printBackground: true,        // background印刷
        landscape: false,             // 横向き印刷
        // pageRanges:1,              // 印刷範囲
        // format: 'A4',              // 用紙フォーマット(Letter, Legal, Tabloid, Ledger, A0~A5)
        width: width + 'px',          // 用紙の幅(px,in,cm,mm)
        height: height + 'px',        // 用紙の高さ(px,in,cm,mm)
        // margin: {top: '10mm', right: '10mm', bottom: '10mm', left: '10mm'},
    });
    await browser.close();

    const base64 = pdf.toString("base64");
    return {
        statusCode: 200,
        headers: {
            'Content-Length': Buffer.byteLength(base64),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=output.pdf'
        },
        isBase64Encoded: true,
        body: base64
    };
};

Lambdaの本体です。
Puppeteerを動かしURLへアクセス、
その後PDFを返すよう記述しています。
後々、叩いた際にオプションを投げ渡して反映させるよう追記していこうかと考えています。

ローカルでテスト

$ docker-compose build

コンテナをビルド

$ docker-compose up -d

コンテナをアップ

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"trigger"}'

テストで投げて、レスポンスを確認

コンテナ&Lambdaのソースコード

Github

ECRへプッシュ

バージニア北部が安定とのことでしたので、region は us-east-1 を選択しました。
ECR へアクセスし使用方法を選択します。

リポジトリを作成をクリックし、リポジトリ名を入力しリポジトリを作成します。
可視性設定は必ずプライベートを選択してください。

ECRへプッシュするためリポジトリ一覧から先ほど作成したリポジトリを選択し、プッシュコマンドの表示ボタンをクリックします。

ターミナルからプッシュします。この時利用するプロファイル情報(–profile)には注意してください。

$ aws ecr get-login-password --profile [profileName] --region [region] | docker login --username AWS --password-stdin [accountId].dkr.ecr.[region].amazonaws.com
$ docker build -t aws-lambda-puppeteer-pdf .
$ docker tag aws-lambda-puppeteer-pdf:latest [accountId].dkr.ecr.[region].amazonaws.com/aws-lambda-puppeteer-pdf:latest
$ docker push [accountId].dkr.ecr.[region].amazonaws.com/aws-lambda-puppeteer-pdf:latest
                            

Lambdaの作成

Lambda 関数の作成をクリックし、イメージ選択から先ほどプッシュしたイメージを選択すればOKです。

API Gatewayの作成

作成したLambda管理画面から「トリガーを追加」ボタンをクリック

API Gatewayを選択し、追加をクリックするだけでOKです。

メモリの割り当てを 1024MB, タイムアウトを20秒に設定し発行されたURLからPDFが生成されることが確認できます。 今後は Puppeteer に URL,オプションを渡して生成できるようにして使い回していこうかなと思います。

まとめ

  • コンテナ型Lambdaはイメージサイズが10GBのため巨大な処理が可能
  • ローカルでコンテナを立ち上げて検証も簡単
  • デプロイも簡単

負荷軽減のため利用したり、サービスや処理を切り分けるために利用してみてはいかがでしょうか。 コンテナ型 LambdaでPHPを稼働させたもののまとめも今後記事にできればと思います。

Related article

おすすめ関連記事