AWS LambdaでSVGの日本語を文字化けさせずにラスタライズ変換する

# モチベーション

SVGから別のラスタライズ形式(ここではPNGとする)への変換をAWS Lambdaで行いたい。 ただし、この手の変換ではフォントを指定しないと日本語が文字化けすることが多々ある。
そのための方法についてを調査して、自分なりにこれで行く、という方針を定めたのでここにメモする。

## AWS Lambda Layer とは

今回は環境設定に AWS Lambda Layer を利用しているので最初にこれの説明を行う。
AWS Lambda Layer は一言でいえば複数のLambdaで共用可能なリソースセット。 LambdaごとにどのLayerを利用するか最大で5個まで指定できる。
より具体的に言うと、事前に S3 にアップロードしたコンテンツを Lambda の実行時に `/opt` 以下に展開してくれるサービス。 1 Lambda の容量制限は 50MB だが、Layer + Lambda の場合合計で 250MB まで可能。
一般的なユースケースは、特定プログラミング言語のライブラリであったり、事前にLambda用の環境で make したバイナリファイルなどをアップロードしておき、メインの関数が肥大化しないような設計を可能とする。
今回の検討段階で上がったものは、実行可能バイナリ( ImageMagick の `convert` など)を作成している。

## 調査の変遷

- AWS Lambda で ImageMagick を利用する
  - [そのために用意されたAWS Lambda Layer](https://github.com/serverlesspub/imagemagick-aws-lambda-2) を利用して、Lambda上で `convert -f [fontファイルパス] [SVGファイル] [PNGファイル]` を実行する
  - Python の場合なら `subprocess.check_call(['convert', '-f', ...])` のようにする
  - 上記 Layer を使った場合、 `/opt/bin/convert` と実行ファイルが作られ、`/opt/bin` 以下に自動的にパスが通るようになる
  - ただ、上記のGithubを見れば分かるが、上記のLayerのデフォルトセットではSVGファイルを扱えないので、別途ビルドする必要がある
- rsvg-convert を利用する
  - [そのために用意されたAWS Lambda Layer](https://github.com/serverlesspub/rsvg-convert-aws-lambda-binary) を利用して、Lambda上で `rsvg-convert -o [PNGファイル] [SVGファイル]` を実行する
  - 残念ながら、このコマンドではフォントパスを指定するところがない
  - fontconfig を利用しているので、適切に fontconfig で利用するフォントのパスを指定できれば日本語でのラスタライズが可能

これらの方法を検討し、

- Githubにコミットされたままの make スクリプトが利用可能
- Fontセットはファイルサイズが大きいので、Lambda内に入れるよりは、Layerの中に入れてしまったほうが良いと考えた

ため、後者のAWS Lambda Layerをカスタマイズすることにした。





## Layerのビルド方法

前提として、make, git, docker がローカルにインストールされていて、docker サービスが動いていること。
Lambda が動作している OS の Docker Image を元にビルドすることで、AWS Lambda でも動作するバイナリをビルドすることができる。

```bash
git clone git@github.com:serverlesspub/rsvg-convert-aws-lambda-binary.git
cd rsvg-convert-aws-lambda-binary
# ビルド開始, 割と時間がかかる
make all
```

FTPで取得しているところが curl でダウンロードできなかったので https のURLに書き換えたところがあったが、基本はこれそのままでビルド可能です。
ただ、結構バージョンが古いので、新しいバージョンにしたい場合は Makefile_Rsvg の先頭にバージョン定義があるので、ここを書き換えると良いです。

make が完了した後、 `result` 以下が成果物です。 このディレクトリがAWS Lambda の `/opt` 以下にマウントされるようになります。 補足として、Docker内でビルドされているため owner は root になっている点に注意してください。


## FontConfig の設定と Font のコピー

このレイヤーで利用するフォントと `fonts.conf` を `result/fonts` 以下に設置します。
今回は以下のように配置しました。

検証用として `/usr/share/fonts/opentype` 以下からサブセットをコピーしましたが、自分の環境で行うときは、自分の利用するフォントを配置してください。
また、AWS Lambda Layerを利用する場合、Lambda と合わせても合計容量は 250MB までですので、必要なフォントを厳選するようにしてください。

```bash
fonts
├── fonts.conf
└── opentype
    └── noto
        ├── NotoSansCJK-Bold.ttc
        ├── NotoSansCJK-Medium.ttc
        ├── NotoSansCJK-Regular.ttc
        └── NotoSansCJK-Thin.ttc
```

`fonts.conf` は [フォーラムの記事](https://forums.aws.amazon.com/thread.jspa?messageID=776307) を参考に、このディレクトリをフォントディレクトリとして読み込むように設定します。

```xml:fonts.conf
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <dir>/opt/fonts/</dir>
  <cachedir>/tmp/fonts-cache/</cachedir>
  <config></config>
</fontconfig>
```

## AWS Lambda Layer のデプロイ

### 事前準備

以下の通り make コマンドでデプロイしますが、以下の事前準備が必要です。

- awscli のインストールが必要です。
- 該当AWSアカウントに default profile でアクセス可能であること
  - もし profile を使いたい場合、 Makefile の該当コマンドに `--profile [自分のプロファイル名]` を足してください
- Layerとして利用する zip ファイルを配置する S3 Bucket を作成しておくこと

また、デフォルトのテンプレート設定では node10.x でしか利用できないようになっているので、自分の利用するランタイムに合わせた設定を `template.yaml` に書き加えてください。 例えば、 Python3.8 (※3.7以前は Base が Amazon Linux であり、このレイヤーは利用不可）の場合は以下のように `CompatibleRuntimes` に `python3.8` を書き加えます。

```yaml
Resources:
  RsvgConvertLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: rsvg-convert
      Description: Static build of rsvg-convert (librsvg) for Amazon Linux 2
      ContentUri: result/
      CompatibleRuntimes:
        - nodejs10.x
        - nodejs12.x
        - python3.8
      LicenseInfo: https://gitlab.gnome.org/GNOME/librsvg/blob/master/COPYING
      RetentionPolicy: Retain

```

### AWS へのデプロイ

準備ができたら、以下のコマンドを実行します。

```bash
make deploy DEPLOYMENT_BUCKET=[準備したS3 Bucket]
```

これで現在の設定から必要なCloudFormation用テンプレートを作成して、これを使って新しいスタックをデプロイし、新しいLayerを作成することができます。

なお、make の性質から `result` や `build` のファイルの中身を変えただけでは更新してくれないことがあります。
その場合、`sudo rm -f build/output.yaml` として、ファイルの状態を変化させてから再度デプロイしましょう。


## AWS Lambdaの設定

`rsvg-convert` を利用する Lambda 関数で以下の設定を行います。

- 関数に今回デプロイしたレイヤー (`rsvg-convert`) を設定する
    - AWSCLI: `aws lambda update-function-configuration --function-name my-function --layers arn:aws:lambda:us-east-2:123456789012:layer:my-layer:3` のようにして設定
    - Management Console で関数を開き、デザイナーの関数の下に Layer というところがあるので、ここをクリックした後に画面乗に現れる「追加」ボタンから設定
- 関数の環境変数に `FONTCONFIG_PATH=/opt/fonts` を設定する
    - これがないとアップロードしたフォントは読み込まれない
 

## 実行

以上の設定を行った AWS Lambda 上のコードでコマンドを実行すれば大丈夫です。
例えば、Python 3.8の場合だと、以下のような利用方法が考えられます。

```python
import subprocess

def handler_name(event, context):
    # 何らかの方法で svg を /tmp 以下に持ってくる
    svgpath = '/tmp/input.svg'
    pngpath = '/tmp/output.png'
    subprocess.check_call(['rsvg-convert', svgpath, '-o', pngpath])
    # 生成した PNG ファイルを S3にアップロードするなどして保存
    return
```


# まとめ

最初からフルビルドの ImageMagick インストール済のインスタンス用意したほうが早かったような気はします、が、Lambdaの安定性と未使用時のインフラコストは捨てがたかったのでそれなりに調査しました。

ただ、この機会に AWS Lambda Layer を便利に使う方法について知ることができたので良かったです。