WeasyPrintをAWS Lambdaで使用するアプリケーションをAWS CDKに移植してみた

WeasyPrintをLambda Layer化する処理をCDKに移植しました。
2023.04.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。サービス部の武田です。

以前臼田がWeasyPrintをAWS Lambdaで使用するエントリを書きました。ここではWeasyPrintはLambda Layersとして実装し、それを利用するアプリケーションをLambda関数として実装していました。

今回はこれをAWS CDKに移植してみたので、ポイントなどを紹介します。

なおソースコード一式はGitHubに上げてあります。

TAKEDA-Takashi/weasyprint-cdk: weasyprint-cdk

挙動についておさらい

アプリケーションとしては大きく変わっていません。一部オブジェクトのキーなどを変えたくらいです。次のコマンドでAWSにデプロイできます。

$ npm install
$ npx cdk bootstrap
$ npx cdk deploy

あとはマネジメントコンソールでLambda関数にアクセスし、テスト実行します。

S3バケットにレポートが生成されます。

中身も問題ありません。

移植のポイント

CDKへ移植にするにあたって一番のネックは、Lambda Layersのビルドをどうするか、でした。CDKでは、ランタイムがPythonのLayerを組み込む場合@aws-cdk/aws-lambda-python-alphaモジュールを使用します。このモジュールはLayerのビルドにDockerを使用します。

一方で、cloud-print-utilsで用意されているMakefileでは、Layerのzipを作成するにあたってDockerを使用しています。そのためCDKから実行するDockerイメージの中で、さらにDockerを使用する必要がありました。私は経験がなかったのですが、 Docker in Docker(dind) と呼ばれる利用形態です。今回は、厳密には Docker outside of Docker(dood) らしいのですが、区別せずDocker in Dockerと呼ぶことにします。

この問題を解決するためにいくつかのステップが必要でした。

まず、CDKがデフォルトで使用するDockerイメージではdockerコマンドが使用できません。そのため、docker:rc-dindなどのイメージを使用します。しかし、このイメージではbashコマンドなどが使用できません。そんなわけで、それをベースにして必要なコマンドをインストールするDockerfileを用意します。

docker/Dockerfile

FROM docker:rc-dind

RUN apk add --no-cache bash make zip

CMD []

次に、ボリュームのマウントで問題が起きました。Makefileでは次のようにpwdコマンドを使用してマウントする部分があります。

Makefile

build/fonts-layer.zip: fonts/layer_builder.sh | _build
	docker run --rm \
	  -e INSTALL_MS_FONTS="${INSTALL_MS_FONTS}" \
	    -v `pwd`/fonts:/out \
	    -t lambci/lambda:build-${RUNTIME} \
	    bash /out/layer_builder.sh
	mv -f ./fonts/layer.zip $@

このコマンドはホストで実行する分にはもちろんうまくいくのですが、今回の環境ではうまくいきませんでした。なぜならpwdコマンドで得られるパスはDockerイメージ内部のもので、Dockerとしてはホスト上のパスを渡されないと認識できないからです。そのため偶然そのパスに何かある可能性はありますが、意図したファイルではないでしょう。

具体的には次のエラーが発生しました。

bash: /out/layer_builder.sh: No such file or directory

そのため該当箇所を次のようにpwdコマンドを使用しないものに変更します。${HOST_PATH}は環境変数を参照していますが、これはCDKがビルドする際に渡すホスト上のパスを想定しています。

cloud-print-utils/Makefile

build/fonts-layer.zip: fonts/layer_builder.sh | _build
	docker run --rm \
	    -e INSTALL_MS_FONTS="${INSTALL_MS_FONTS}" \
	    -v ${HOST_PATH}/fonts:/out \
	    -t lambci/lambda:build-${RUNTIME} \
	    bash /out/layer_builder.sh
	mv -f ./fonts/layer.zip $@

一番のポイントであるLambda Layerを作成するCDKのソースは次のようになりました。

lib/weasyprint-cdk-stack.ts

const weasyprintLayer = new lambdaPython.PythonLayerVersion(
  this,
  "WeasyprintLayer",
  {
    layerVersionName: "weasyprint-layer",
    entry: path.resolve(__dirname, "../cloud-print-utils"),
    compatibleRuntimes: [lambda.Runtime.PYTHON_3_8],
    bundling: {
      image: cdk.DockerImage.fromBuild("./docker/"),
      environment: {
        HOST_PATH: path.resolve(__dirname, "../cloud-print-utils"),
      },
      user: "root",
      command: [
        "bash",
        "-c",
        [
          "make build/weasyprint-layer-python3.8.zip",
          "cp build/weasyprint-layer-python3.8.zip /asset-output",
        ].join(" && "),
      ],
      volumes: [
        {
          hostPath: "/var/run/docker.sock",
          containerPath: "/var/run/docker.sock",
        },
      ],
    },
  }
);

bundlingプロパティを指定することで、かなり細かくカスタマイズできます。imageではビルドに使用するDockerイメージを使用しますが、先ほど用意したDockerfileが使用されるようにします。environmentではMakefileで使用される環境変数を設定しています。

commandがDockerコンテナで実行されるコマンドの指定です。ビルドのために使用していたmakeだけでなく、ホストにアーティファクトを連携するため、/asset-outputにzipファイルをコピーしています。これはCDKで使用するDockerコンテナのルールにのっとっています。

volumesでマウントするボリュームを指定します。今回はホストのsockファイルを共有しています。この指定でdoodの環境となり、コンテナの中でDockerを操作します。ちなみに、これを指定しないと次のようなエラーが起きてしまいました。

docker: error during connect: Post "http://docker:2375/v1.24/containers/create": dial tcp: lookup docker on 192.168.5.3:53: lame referral.

今回M1 MacのRancher Desktopを使用しているのですが、他の環境ではこのエラーは起きない可能性があります。よければ試してみてください。

まとめ

WeasyPrintのレイヤー化をCDKで実装してみました。Docker in Dockerbundlingを使用したビルドのカスタマイズなど、これまで経験のなかった範囲ですが、なんとか実現できてよかったです。

料金はほぼ発生しませんが、後片付けは忘れずに。