見出し画像

AWS LambdaでLibreOfficeを実行する

 こんにちは。ディマージシェアの技術担当です。今回はAWS LambdaでLibreOfficeを実行する環境を整備したいと思います。

背景

 要望としては、データをPDF帳票にしたい、の一言です。PDF帳票の実装を好むエンジニアは少ないと思います。PDFを作るライブラリ、HTML、CSSをPDFに変換するライブラリ、その他、どれも癖があり独特のテクニックを強いられます。今回はExcelで作ったテンプレートをLibreOfficeでPDF化する、というアプローチを採用することにしました。
 しかし、LibreOfficeをコマンドラインで実行する際、結構なRAMを消費することがわかりました。サーバに載せるには結構スペックを盛らないとアクセス集中したときに簡単にコケてしまいます。更に、帳票の類は往々にして月初月末と生成されるタイミングは集中するものです。それなら使っただけ課金のLambdaにしよう、という話になったのです。

先人の知恵に頼る

 「Lambda LibreOffice」のワードでgoogle検索したら、やはり同じことを考えてる人が多く居ることがわかりました。日本語でもこちらこちらの記事がヒットしました。しかしながら、いくつかの問題が発生したため、別のアプローチを採用することになりました(本記事後半)。

先人たちのアプローチ

 Lambdaはデプロイパッケージの容量を250MB以内に収める必要があります。しかし、LibreOffice自体は360MBほどあります。これを解決するために、LibreOfficeを圧縮してLambdaレイヤーに設置し、実行時に展開して/tmpの中に押し込むという手法が採られていました。何が何でもLambdaで実行してやるという強い意志を感じました。私も同様の実装を試しました。しかし、次のトラブルに見舞われました。

遭遇したトラブル

(1)遅い
 実行するために圧縮アーカイブを展開する処理が毎度走ります。プログラムが起動するまで10秒ほどかかりました。
(2)メモリ消費
 /tmpにたくさん書き込むので、1回の実行で1200MBほどRAMを消費することがわかりました。
(3)日本語フォントが入らない
 LibreOfficeはOSのフォントを参照します。Lambdaのランタイムには当然載っていません。Lambdaの実行基盤がAmazon Linuxの場合ではfont cacheにねじ込むことが出来ましたが、新しめの言語の実行基盤(Amazon Linux 2)では難しいようでした。LibreOffice自体も、Amazon Linuxで動くパッケージはバージョンが古く、まもなくサポートが切れるAmazon Linuxの基盤に無理やり乗せたLambdaは使いたくない、という結論に至りました。

LibreOfficeが動くDockerコンテナをLambdaにデプロイ

 もろもろのトラブルから、複雑なアプローチでLambdaに実装することを諦め、2020年に公開されたコンテナをLambdaで実行する方法を試してみることにしました。実行基盤をコンテナにして固めてしまえば、もろもろの課題が全て解決するハズです。

Dockerfile
もろもろのランタイムと日本語フォントを入れています。LibreOfficeは動画コンテンツなども動くので、単に帳票を出したいだけ、という用途ではもう少しスリムにできそうです。

# Pull the base image with python 3.8 as a runtime for your Lambda
FROM public.ecr.aws/lambda/python:3.8

# Install OS packages for Pillow-SIMD
RUN yum -y install curl wget tar gzip zlib freetype-devel
RUN yum -y install libxslt \
    gcc \
    ghostscript \
    lcms2-devel \
    libffi-devel \
    libjpeg-devel \
    libtiff-devel \
    libwebp-devel \
    make \
    openjpeg2-devel \
    sudo \
    tcl-devel \
    tk-devel \
    tkinter \
    which \
    xorg-x11-server-Xvfb \
    zlib-devel \
    java \
    ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts \
    && yum clean all

RUN wget http://download.documentfoundation.org/libreoffice/stable/7.2.7/rpm/x86_64/LibreOffice_7.2.7_Linux_x86-64_rpm.tar.gz
RUN tar -xvzf LibreOffice_7.2.7_Linux_x86-64_rpm.tar.gz
RUN cd LibreOffice_7.2.7.2_Linux_x86-64_rpm/RPMS; yum -y localinstall *.rpm;
RUN yum -y install cairo

COPY app.py ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]

app.py
S3へのファイル設置をトリガーにして、libreoffice-outというバケットにpdfを設置するソースを書きました。

import json
import subprocess
import boto3
import os
import urllib.parse

s3 = boto3.resource('s3')
output_bucket = "libreoffice-out"

def handler(event, context):
    if 'Records' in event.keys():
        input_bucket = event['Records'][0]['s3']['bucket']['name']
        input_key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
        in_bucket = s3.Bucket(input_bucket)
    else :
        return 'test finished'

    print(input_key)
    # get S3 Object
    file_path = '/tmp/'+input_key
    in_bucket.download_file(input_key, file_path)

    proc = subprocess.run("/opt/libreoffice7.2/program/soffice --headless --norestore --invisible --nodefault --nofirststartwizard --nolockcheck --nologo --convert-to pdf:writer_pdf_Export --outdir /tmp {}".format("/tmp/"+input_key), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    print('STDOUT: {}'.format(proc.stdout))
    print('STDERR: {}'.format(proc.stderr))

    key_list = input_key.split('.')
    pdf_path = "/tmp/"+input_key.replace(key_list[-1], 'pdf')

    # put S3 Object
    if os.path.exists(pdf_path):
        print('PDF: {}'.format(pdf_path.replace("/tmp/", "")))
        print('Size: {}'.format(os.path.getsize(pdf_path)))
        data = open(pdf_path, 'rb')
        out_bucket = s3.Bucket(output_bucket)
        out_bucket.put_object(Key=pdf_path.replace("/tmp/", ""),Body=data)
        data.close()
    else :
        print("The PDF file({}) cannot be found".format(pdf_path))

    return ''

ランタイムもLibreOfficeも新しめのものを選びました。ローカルではそれっぽく動きました。ECRにpushしてLambdaにデプロイしてみます。

javaldx failed!\nWarning: failed to read path from javaldx\nLibreOffice

ローカルでは出なかったエラーでコケました。調べたところ、$HOME配下に書き込みを行うパーミッションが無い場合にエラーになるようです。/tmpにしか書き込めない制約はコンテナでデプロイしても同じようです。環境変数で、
HOME=/tmp
と指定したら動作しました。

動作確認

(1)速度
 初回実行に限り30秒程度かかりますが、ある程度繰り返し実行すると、3秒ほどで安定しました。
(2)メモリ使用量
 400MB弱で安定しました。
(3)日本語フォント
 実行基盤にフォントを入れたので問題なく出ました。


この記事が気に入ったらサポートをしてみませんか?