月別: 2019年8月

AWS サーバレスでlibreofficeを使ってExcelをPDF変換

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

弊社のプロジェクトのいくつかでは、帳票の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

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

以上です!

Amazon Elastic File System (Amazon EFS) におけるバーストとプロビジョニングの変更を自動化する

クラウド事業部の原口です。

AWSのEFSが発表されてから「これを待っていた!!」というエンジニアの方も多かったと思いますが、割と落とし穴的な制限が多く、
その中の一つとしてEFSの「バースト」と「プロビジョニング」の2つのスループットモードがあります。

バーストスループットモードとは何か。

誤解を恐れず簡単に説明しますと、スマフォの帯域制限みたいな機能となります。
EFSではバーストクレジットと呼ばれる値があり、このクレジットがある間は非常に高速なスループットでデータのやり取りが可能なのですが
クレジットがなくなると制限がかかりめちゃくちゃ低速なファイルストレージになってしまいます。
これはプロダクション環境ではサーバ障害とも言えるレベルでの低速さになります。

バーストクレジットを回復するには?

基本的にファイルストレージを使っていなければ回復していきます。
そのため、EFSの利用頻度がクレジットの回復力を下回る場合はクレジットは減る事がありません。

また、使っているEFS内のサイズによってもクレジットのそもそもの量が異なります。
大きいファイルが多数あるなど、EFS内のデータ量が多ければクレジットのそのものの絶対量が多く付与されます。

これを理解してない場合に起こる問題

基本的には運用時、EFSはバーストスループットモードとして動作する事がほとんどですが、
よくある事例としては「テスト段階では高速で快適に動いていたのに、本番運用していたら急激に低速になり障害があった」という事例です。

CloudWatchのメトリクスに「BurstCreditBalance」というメトリクスがありますが、こちらがバーストクレジットで、
ゼロになると低速なストレージになってしまいます。

f:id:cs_sonar:20190826124939p:plain

上記のようなグラフの場合は運用で徐々にクレジットが減っていってるのでずっと運用しているといつかは障害となりそうです。

プロビジョニングスループット

さて、バーストクレジットを使い果たして低速となってしまった場合はどうすればいいのでしょうか?

もちろん「大きなファイルを作ってバーストクレジットを増やす」というのも一つの解決策ですが、
一般的にはバーストモードからプロビジョニングスループットというモードへ変更する事で対策できます。

プロビジョニングスループットは帯域保証のような機能で50(MB/秒)といった値を設定する形でスループット値を保証させるモードです。
このモードではクレジットなどに関係なく設定した速度は必ず保証されます。

しかし・・・このプロビジョニングスループットは非常に高額です。
1MB/秒 の保証にあたり月額7.2USDもかかります。
件のように50MB/秒を保証させると月額360USD ≒ 38,000円。
ただでさえEFSは高額なのにこの価格は無視できる額ではありませんよね。

プロビジョニングとバーストクレジットを交互に設定をする事でコスト削減を図る

実はバーストクレジットからプロビジョニングスループットに変更すると
プロビジョニングスループット中はバーストクレジットが回復していきます。

f:id:cs_sonar:20190826125825p:plain

8/23の途中からプロビジョニングスループットとしていますが、3日ほどでクレジット最大値まで回復していますね。

つまり、最もコスト効率がいいのは

クレジットを限界まで使い直前でプロビジョニングスループットに変更 。
クレジットが回復したらバーストスループットに変更。

という形となります。

CloudWatch + SNS + lambda を使ったバーストとプロビジョニングの変更を自動化

ここからが本題。
上記の形がコスト効率が最もよいのですがこんなの人の手でぽちぽちやってれないので自動化します。

まずは、バーストとプロビジョニングの変更を行うlambda関数を作成します
バーストモードにするものとプロビジョニングにするものの2つ

EFS-to-bursting

console.log('Loading function');
var https = require('https');
var http = require('http');
const aws = require('aws-sdk');
var efs = new aws.EFS();
exports.handler = (event, context) => {
(event.Records || []).forEach(function (rec) {
if (rec.Sns) {
var message = JSON.parse(rec.Sns.Message);
var params = {
FileSystemId: message.AlarmDescription, /* required */
ThroughputMode: 'bursting'
};
efs.updateFileSystem(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else     console.log(data);           // successful response
});
// ここでslack等に通知するといいと思う
}
});
};

EFS-to-provisioned-iops

console.log('Loading function');
var https = require('https');
var http = require('http');
const aws = require('aws-sdk');
var efs = new aws.EFS();
exports.handler = (event, context) => {
(event.Records || []).forEach(function (rec) {
if (rec.Sns) {
var message = JSON.parse(rec.Sns.Message);
var params = {
FileSystemId: message.AlarmDescription, /* required */
ProvisionedThroughputInMibps: '10', // プロビジョニング10MB
ThroughputMode: 'provisioned'
};
efs.updateFileSystem(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else     console.log(data);           // successful response
});
// ここでslack等に通知するといいと思う
}
});
};

FileSystemId: message.AlarmDescriptionの部分ですが
受け取ったCloudWatchアラームにはどのEFSがアラームを出したか、というものが取得しずらかったので
CloudWatchアラームのDescriptionにEFSのIDを載せる形にしました

SNS(Simple Notification Service)のトピックを作成してlambdaに連携します

SNSは通知先にメールやhttpなどだけでなくlambdaに通知できます。
ですので、バーストモードにするlambdaに連携するトピックと、
プロビジョンドにするlambdaに連携するトピックの2つを作成します。

f:id:cs_sonar:20190826131112p:plain

cloudwatchにてアラームを作成

BurstCreditBalanceを監視し、上記作成のSNSに通知するアラームを作成します。
こちらも、
・バーストクレジットが一定数以上の時 -> プロビジョンモードにするSNSに発火
・バーストクレジットが一定数以下の時 -> バーストモードにするSNSに発火
という形で2つ作成します。

f:id:cs_sonar:20190826131630p:plain

lambdaの所でも書いた通り、DescriptionにはEFSのIDを入力します

f:id:cs_sonar:20190826131750p:plain

バーストとプロビジョニングの変更を自動化できました

このように複数EFSあると便利さがわかりますね!

f:id:cs_sonar:20190826132003p:plain

クレジットが減るとプロビジョニングになり、クレジットが充足するとバーストモードになる形です

注意点

EFSの制限で、スループットモードの変更は1日に1回までしかできません!
そのため、1日でバーストクレジットを使い果たしてしまうような環境ではずっとプロビジョニングモードで動かす必要があります。

本日は以上で!

© SEEDS Co.,Ltd.