クラウド事業部の川勝です。

弊社のプロジェクトのいくつかでは、帳票のPDF出力にExcelで作成したものをPDF変換かけて使用するというものがあります。
Excel->PDF変換にはlibreoffceのheadlessモードを使用しているのですが、今回その仕組みを汎用的に使えるようにサーバレスで構築してみたのでご紹介いたします。

要件

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

構成

f:id:seeds-std:20190827191927p:plain

Serverless libreoffice

まずAWS Lambdaでlibreofficeが使えるのか?(外部コマンドあるか)というところからはじめました。
外部コマンドでsofficeがあるかどうか。
ちょっと古い2014年の記事ですがどうもなさそうです…

AWS Lambdaをいろいろ暴く – Qiita

(ちなみにamazon linuxベースとのことなので大体の外部コマンドは使用可能ですね。)

自力でインストールするしかない、、できるのか、、?と調べていたところ以下を発見いたしました。

serverless-libreoffice/STEP_BY_STEP.md at master · vladgolubev/serverless-libreoffice · GitHub

こちらのStep:1ではlibreofficeをコンパイルするところから始まっていますが、リポジトリ内にコンパイル済みのファイルがありますのでそちらを使用します。(Step:2)

S3にserverless-libreofficeをアップロード

$ aws s3api create-bucket --bucket lambda-libreoffice-demo
$ curl -L https://github.com/vladgolubev/serverless-libreoffice/releases/download/v6.1.0.0.alpha0/lo.tar.gz -o lo.tar.gz
$ aws s3 cp lo.tar.gz s3://lambda-libreoffice-demo/lo.tar.gz --acl public-read-write

これで前準備は完了。 

Lambda実行時にアップロードしたファイルを展開して使用できるようにします。
今回はgoで実装していきます。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
}

main.goのinitでs3から取得したファイルを /tmp に展開します。
initで呼び出すことでlambdaコンテナの起動時のみ実行されます。
注意点はLambdaでは /tmp 以下にしか書き込み権限がないのと容量制限(500MB)があります。

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数のためにディスクにスクラッチスペースが必要な場合はどうすればよいですか?

各 Lambda 関数では /tmp ディレクトリに 500 MB の容量 (非永続型) を割り当てることができます。

ひとまずこれで /tmp/instdir/program/soffice として実行可能になります。
受け取ったExelファイルは /tmp/sample.xlsx にファイルとして保存しているとして、以下の様な感じで実行できました。

const convertCommand = "/tmp/instdir/program/soffice --headless --invisible --nodefault --nofirststartwizard --nolockcheck --nologo --norestore --convert-to pdf --outdir /tmp %s"
command := fmt.Sprintf(convertCommand, "/tmp/sample.xlsx")
exec.Command("sh", "-c", command).Run()

/tmp/sample.pdf が生成されるので、これをbase64エンコードしてjson形式で返却します。

…がしかし、実行はできましたが、このままだと日本語が文字化けてしまいます。
日本語フォントに対応が必要そうです。

日本語フォント

「lambda 日本語フォント」とかで調べていると以下の記事にあたりました。

AWS LambdaでPhantomJS日本語フォント対応 | RCO Ad-Tech Lab Blog

Lambdaの実行環境にフォントを追加する – Qiita

ようはfont-cache生成できればよさそうです。
上記記事では生成したfont-cacheをデプロイパッケージに含めていましたが、initでlibreofficeの展開もしているのでそこでfont-cache生成したらいいかな、と思ったのでその方向で実装しました。

initに追加します。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
os.Setenv("HOME", os.Getenv("LAMBDA_TASK_ROOT"))
cmd := "mkdir -p /tmp/cache/fontconfig && fc-cache " + path.Join(os.Getenv("HOME"), ".fonts")
exec.Command("sh", "-c", cmd).Run()
}

.fonts ディレクトリに使用するfontファイルをデプロイパッケージに含めてlambdaにアップロードします。

以下ディレクトリ構成でzipに固める

.
├── .fonts
│   ├── hoge.ttc
│   └── fuga.ttc
├── .fonts.conf
└── main

main goのbuild済みファイルです。
.fonts.confは参考URLのまま以下の設定にしています。

<fontconfig>
<cachedir>/tmp/cache/fontconfig</cachedir>
</fontconfig>

これで実行するとPDFに日本語が含まれていてもOKになりました!

Timezone

構築後に社内で試してもらっていると日付関係が変?という報告がありました。
LambdaのデフォルトtimezoneはUTCなのでExceleで =Now() とかするとUTCの時刻になるようです。
これは対応が簡単で、lambdaの環境変数設定で解決。
TZ をkeyにして設定可能でした。

key: TZ
value: Asia/Tokyo

ちなみに日付の並び順は変わらなかったので、Excel側でフォーマット指定してもらうことにしました…

=TEXT(NOW(),"yyyy/MM/dd hh:mm:ss")

これにて無事日本語が含まれるExcelデータもPDF変換可能です!

その他ハマったところ

バイナリメディアタイプ

要件のところで書いていた話

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

API Gatewayの設定でバイナリメディアタイプを設定することで、ファイルアップロードしたらPDFがそのままダウンロードできる…と思ってやっていたのですが、どうにもうまくいかず断念しました。(これは自分のgo言語の力量不足かもしれませんが..)

あと、ブラウザからではリクエストヘッダーに Accept: application/pdf が付与されないためCloudFrontをかまさないといけない、、、ということなので今回はjsonでやり取りでいいかな、となったのも理由です。

送信するときにExcelデータをbase64エンコードして、変換したPDFもbase64エンコードして返却させています。
送信側でプログラム書く場合はこの方がシンプルに構築できるのでいいかな、と感じています。

API Gatewayのタイムアウト

API GatewayのタイムアウトはLambdaより短いです。
Lambda ・・・最長15分

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数はどれくらいの時間実行できますか?

AWS Lambda 関数は、1 回あたりの実行時間を最長 15 分に設定することができます。タイム> アウトは 1 秒から 15 分までの間で任意に設定できます。

API Gateway ・・・最長29秒

Amazon API Gateway の制限事項と重要な注意点 – Amazon API Gateway

統合のタイムアウト・・・Lambda、Lambda プロキシ、HTTP、HTTP プロキシ、AWS 統合など、すべての統合タイプで 50 ミリ秒~29 秒。

また制限の解除も対象外です。

Lambdaのデフォルトのメモリ(128MB)だとタイムアウトすることが多かったのでしたので256MBで実行するようにしました。

最後に

サーバレス開発で今回のように外部コマンドもLambda上で展開してしまえば結構なんでもできそうだなと思いました。
API gatewayのバイナリメディアタイプに関しては引き続き調べていきたいところです。

あと今回はサーバーレスアプリ自体にはあまり触れませんでしたが SAM CLI を使用して開発、デプロイしています。

GitHub – awslabs/serverless-application-model: AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications

このあたりの話はまたの機会に…

以上です!