記事本文

test

CloudflareでCSPを設定

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 のトリガー

(2022/01/12現在)

  • 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

AWS Lambda 4つのデプロイ方法

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

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

■ デプロイパッケージの割り当てサイズ制限 * zip形式で直接アップロードする場合は50MB * 非圧縮パッケージの場合は250MB

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

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

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 app.js package*.json ${LAMBDA_TASK_ROOT}
RUN npm install

CMD ["app.handler"]

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

■ package.json, package-lock.json

コンテナ型Lambdaであるため、パッケージのサイズは気にせず、純粋なPuppeteerのみ利用するよう記述しています。
日本語化の問題もあるため純粋な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

Lambdaの本体です。
Puppeteerを動かしURLへアクセス、
その後PDFを返すよう記述しています。

不完全ですが、叩いた際にオプションを投げ渡して反映させるよう追記していこうかと考えています。

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
    };
};

ローカルでテスト

コンテナをビルド

$ docker-compose build

コンテナをアップ

$ docker-compose up -d

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

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

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

testtest

ECRへプッシュ

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

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

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

ターミナルからプッシュします。この時利用するプロファイル情報(--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

image

Lambdaの作成

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

API Gatewayの作成

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

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

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

まとめ

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

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