S3 Object Lambda を使って処方箋プレビューに透かしを入れる
こんにちは。医療プラットフォーム本部プロダクト開発室エンジニアの中畑です。主にオンライン診療・服薬指導アプリ CLINICS の開発を担当しています。
今回は CLINICS アプリ内で扱う処方箋プレビューに透かし(watermark)を入れた話を紹介したいと思います。なぜ実施したのか、実装方法、パフォーマンスチューニングの 3 本立てでお送りしたいと思います。
課題と解決方針
まず、なぜ処方箋プレビューに透かしを入れることにしたのか。
CLINICS では診察後に患者が希望すると、かかりつけ薬局支援システム Pharms を導入している調剤薬局にてオンライン服薬指導を受けることができます。その際に医療機関から処方箋の画像ファイルや PDF をアップロードし、患者は CLINICS アプリを通じて、オンライン服薬指導を受けたい調剤薬局に処方箋データを送る必要があります。
患者はオンライン服薬指導を予約する前に医療機関からアップロードされた処方箋画像をプレビューできます。この処方箋プレビューは原本とは扱いが異なるため、患者がアプリ内でそれぞれを明確に区別できるような対応を入れる必要があり、その手段として処方箋プレビューに透かしを入れることにしたというのが理由となります。
実現・実装方法
まずは画像に透かしを入れる方法を考え、それからシステム上でどのタイミングで透かしを入れるか検討しました。前提条件ですが、処方箋プレビュー画像は以下のような形で保存されています。
- フォーマット: JPEG, PNG, GIF, PDF(画像以外もある)
- 保存場所: S3
画像に透かしを入れる方法
透かしは技術的には 2 枚の画像をアルファブレンド1を使って合成した画像となります。
アルファブレンドの実現方法は言語やライブラリによっていろいろありますが、今回は処方箋を扱うシステムが Go を利用しているので、Go の以下の標準ライブラリを使って実装しました。また、画像には向き(orientation
)の情報が保持されているのですが、向きを意識しないと合成時におかしくなることがあります。端末やツールによって取り扱いが異なる部分が多いので、それを解消してくれる disintegration/imaging
パッケージを利用しました。
- image パッケージ: ベースライブラリ
- ximage パッケージ: 描画処理
- disintegration/imaging パッケージ: 読み込み時に向きを補正
詳細は省きますが、以下のようなコードで合成できます
func Watermark(srcFile string) {
// 元画像
imgb, _ := os.Open(srcFile)
var img image.Image
img, _ = imaging.Decode(imgb, imaging.AutoOrientation(true))
defer imgb.Close()
// 上に重ねる透かし画像
wmb, _ := os.Open("watermark.png")
watermark, _, _ := image.Decode(wmb)
defer wmb.Close()
// 透かしを配置する場所 (画像サイズによって異なるため、実際は計算が必要)
watermarkRect := image.Rectangle{image.Point{100, 100}, image.Point{200, 300}}
// 合成処理(ここではバイリニア補間で実施)
b := img.Bounds()
m := image.NewNRGBA(b)
draw.Draw(m, b, img, image.ZP, draw.Src)
draw.BiLinear.Scale(m, watermarkRect, watermark, watermark.Bounds(), draw.Over, nil)
// 合成画像の出力(JPEG 出力。他のエンコーダーを利用すれば他の画像形式も可)
imgw, _ := os.Create("/tmp/dest.jpg")
jpeg.Encode(imgw, m, &jpeg.Options{Quality: jpeg.DefaultQuality})
defer imgw.Close()
}
PDF に透かしを入れる方法
また、今回の要件には PDF ファイルにも透かしを入れる必要がありました。PDF にはレイヤーという概念があり、元の PDF に対して透かしを入れるレイヤーを追加すれば実現できます。Go には PDF を標準で扱えるパッケージがなかったので、今回は pdfcpu を利用しました。
透かしのレイヤーを追加する関数が用意されており、以下のコードだけで実現できて大変便利です。2
wm, _ := api.ImageWatermark("watermark.jpg", "sc:.8 rel, rot:0", onTop, update, pdfcpu.POINTS)
api.AddWatermarksFile("src.pdf", "dest.pdf", nil, wm, nil)
S3 画像に透かしを入れる方法
AWS を利用していると S3 に各種ファイルを保存することも多いと思います。 基本的には S3 画像を読み込んで透かし処理をすれば実現できますが、AWS には S3 のレスポンスを加工するマネージメントサービスがいくつかあり、今回は S3 Object Lambda を利用しました。
S3 Object Lambda を利用することで S3 からファイルを取得する際に Lambda による処理を実行したうえで返却することができます。これを利用して、元の画像に Lambda で透かしを入れて返却することができます。
処方箋プレビューに透かしを入れる前の構成は以下のようなイメージです。
- アプリサーバーにて処方箋プレビューの S3 presigned URL(署名付き URL)を発行
- クライアントに返す
- クライアントは受け取った URL にアクセス。処方箋プレビューが表示される
これに S3 Object Lambda を利用して透かしを入れると以下のようになります。
- アプリサーバーにて処方箋プレビューの
S3 Object Lambda Access Point
の presigned URL(署名付き URL)を発行 - クライアントは受け取った URL にアクセス
S3 Object Lambda Access Point
は透かし処理を行う Lambda 関数を実行。その際にS3 Access Point
の署名付き URL を発行し、リクエストパラメータに設定Lambda Function
は受け取った URL にアクセスし処方箋プレビューの元画像を取得して透かしを入れて返却- 透かし入りの処方箋プレビューが表示される
新たに S3 Object Lambda Access Point
, Lambda Function
, S3 Access Point
を用意する必要があって少しややこしいですが簡単に説明すると以下のような役割のものとなります。
透かし処理を実際呼び出す際は、アプリケーション側からは S3 の presigned URL から S3 Object Lambda Access Point の presigned URL に切り替えるだけなので、透かしありなしの切り替えも簡単です。
S3 presigned URL:
https://[bucket-name].s3.ap-northeast-1.amazonaws.com/prescription.jpg?[signature]
↓ S3 Object Lambda Access Point presigned URL:
https://[s3-object-lambda-access-point]-[account-id].s3-object-lambda.ap-northeast-1.amazonaws.com/prescription.jpg?[signature]
S3 Object Lambda を利用することで以下のようなメリットがあります。
- 追加のインフラ構築が不要
- 既存サーバーのリソース追加や、新たに透かし処理用のサーバーを用意する必要がない。既存のアプリサーバーへの影響を軽微にできる
- Lambda が自動的にスケールを実施してくれるので負荷対策が容易
- 利用した分だけの課金となる(S3 Get + Lambda + S3 Object Lambda の利用料)
- 透かしを入れた処方箋プレビュー画像を事前に用意する必要がない
- 事前に用意するとリリース前に保持した画像に対しても処理する必要が出てくる
- (ただし、今回は特定のユーザーのみのアクセスなので不特定多数のアクセスが見込まれる場合は事前生成したほうがよいかもしれません)
もちろん画像だけではなく、テキストデータなど他のものにも利用できるので、S3 に保管した機密情報等をフィルタリングして返したい用途などにもマッチします。
AWS 公式ドキュメントに詳細な解説と様々なユースケースのチュートリアルがありますので、興味がある方は是非御覧ください。
パフォーマンス・チューニング
ここまでで、処方箋プレビューに透かしを入れる処理自体は完成しました。ここで気になってくるのがパフォーマンスについてです。今までは S3 の画像を表示するだけでしたが透かし処理に S3 Object Lambda を使うことになったため、速度と料金が気になるところです。
様々なチューニングポイントがありますが、今回は Lambda のメモリ設定に焦点を絞ってお話したいと思います。
Lambda の CPU パフォーマンス
Lambda はメモリ設定しかできませんが、メモリ量に比例して CPU 性能が向上する仕組みとなっています。 公式ドキュメント上では 1769MB あたり 1vCPU であるとされていて、MAX の 10240MB で 6vCPU 使えるとしています。実際に Lambda の vCPU の設定を調べたブログ (SENTIA tech blog) では以下のようになっていたそうです。
Memory | vCPUs |
---|---|
128 - 3008 MB | 2 |
3009 - 5307 MB | 3 |
5308 - 7076 MB | 4 |
7077 - 8845 MB | 5 |
8846 - 10240 MB | 6 |
ただし、メモリをいくら増やしたところで関数自体がマルチスレッド・マルチプロセス化されていない場合はパフォーマンス向上が見込めないということにもなります。適切に並列処理が実装されていれば、vCPU が増える境目で大きな性能向上が期待できます。
Lambda の料金
東京リージョン(ap-northeast-1)において、2022 年 10 月現在は以下のような料金となります。3
- 無料枠: 1 ヶ月あたり 100 万リクエスト、40 万 GB-秒(仮に 1 GB で動かしたとして 40 万秒使える)
- 128 MB だと 320 万秒、 10240 MB だと 4 万秒
- 有料枠: 100 万リクエストあたり 0.20 USD, GB-秒あたり 0.00001666667 USD
- 1 億リクエストあたり 20 USD
- 128MB で 1 万秒 使うと 0.02083 USD
- 1024MB で 1 万秒 使うと 0.16666 USD
- 上記は x86(amd64) の値段、arm だと 2 割引
- arm は CPU パフォーマンスも 2 割向上すると言われているので、可能であれば arm を選択すると更にコスパ向上が期待できる
このように見てみると、サーバーやコンテナを利用した場合はどんなに最小構成でも冗長化を考えると数十ドルはかかるので、特に頻繁に実行されない機能は Lambda によるサーバーレス化を検討すると良さそうです。
また、時間あたりで料金がかかるということは、API などネットワークアクセスで時間を使った場合も料金がかかることに注意が必要です。外部 API のパフォーマンスに依存してしまうので、なるべく Lambda は CPU を使った処理が支配的になるとコスパよく利用することができます。
ベンチマーク
Lambda のパフォーマンス計測には、AWS Lambda Power Tuning を利用しました。AWS Lambda Power Tuning は Step Functions と Lambda を使って Lambda の速度とコストを計測できるツールです。
Step Functions と Lambda をデプロイした上で、Step Functions の state machine に計測したい Lambda の ARN, memory 設定, 実行回数, payload を入力することでベンチマーク結果を取得できます。 実行すると最終結果に URL が出力され、その URL をブラウザで表示することで、グラフで結果とスコアボードで結果が可視化されます。また 2 つの結果を比較して表示することもできますので改善後に比較することも容易です。4
AWS Lambda Power Tuning の導入
リポジトリのドキュメントに従い導入していきます。
今回は AWS Serverless Application Repository(SAR) を使って導入しました。AWS Serverless Application Repository(SAR) とはその名の通り、サーバーレスアプリケーションを管理するリポジトリですが、組織内に限らず公開することも可能で、他の人も再利用可能になっています。また、導入する際は公開ページの Deploy ボタンをポチるだけで自身の AWS アカウント上にデプロイされ、 CloudFormation の Stack として管理されます。削除も CloudFormation の Stack を削除するだけで実施できます。
AWS Lambda Power Tuning はこちらから自身のアカウントにデプロイできます。 また、AWS や個人だけでなく Datadog や New Relic など他のパブリッシャーもサーバーレスアプリケーションを公開しているので、他にも使いたいものがないか探してみるとよいかもしれません。
AWS Lambda Power Tuning の実行
デプロイが完了すると、AWS Step Functions 上に state machine があるので、state machine にパラメータを入力することで計測ができます。リポジトリのドキュメントにいくつかやり方が記載されています。ここでは 実行パラメータを定義した json ファイルを用意してスクリプトから実行する方法を紹介します。
まずは、AWS Lambda Power Tuning リポジトリを git clone
し、scripts/sample-execution-input.json を編集します。
{
"lambdaARN": "arn:aws:lambda:ap-northeast-1:123456789012:function:lambda-performance-tuning",
"powerValues": [128, 256, 512, 832, 1024, 1536, 1769, 1770, 2048, 3008],
"num": 100,
"payload": {
"body": "{\"id\": \"123\",\"name\":\"performance\"}",
"path": "/api/performance",
"httpMethod": "GET",
"isBase64Encoded": false,
"multiValueHeaders": { "Accept": ["application/json"] }
},
"parallelInvocation": false
}
入力値の詳細は以下の通りです。5
- lambdaARN: Lambda Function の ARN
- powerValues: 計測したい Lambda のメモリを指定
- num: メモリごとの Lambda 実行回数
- payload: Lambda Function にわたす Event JSON の内容
- parallelInvocation: ベンチマークを並列実行する
次に、 scripts/execute.sh を実行すると、ベンチマークが開始され、最後に結果 URL が出力されます。デプロイ時の設定によって、CloudFormation の Stack Name が違う場合もありますので、必要に応じて書き換えてください。
$ sh ./scripts/execute.sh
-n Execution started...
-n .
:
-n .
SUCCEEDED
Execution output:
{"power":128,"cost":8.4E-9,"duration":3.6706666666666674,"stateMachine":{"executionCost":4.0E-4,"lambdaCost":1.0178708203125003E-4,"visualization":"https://lambda-power-tuning.show/#gAAAAQACQAMABAAG6QbqBgAIwAs=;NOxqQP9GmkDFII5AbxKBQLErfECGXW1AWDmGQNNNjkBpSo1ARf1nQA==;l08QMn1jtDJ9YzQz1pCSM5dPkDNjd9gzb9AbNPzmGzR9YzQ05vRTNA=="}}
計測方法は以上、簡単ですね 👌
AWS Lambda Power Tuning の結果確認
計測結果を確認します。関数によって傾向は異なりますが、ここでは 2 種類のパターンを紹介します。
DB や API アクセスなど外部処理が支配的なケース(Network-intensive)
Lambda 内の処理が外部処理で占めている場合は、以下のようなグラフになります。 赤色の実行時間がどのメモリ設定でも 4ms 前後と大きな変化がなく、青色のコストだけが右肩上がりになっています。 この場合は、Lambda の最低メモリである 128MB が最適ということになります。
CPU 実行時間が支配的なケース(CPU-intensive)
Lambda 内の処理が内部処理で占めている場合は、以下のようなグラフになります。透かし処理は画像合成処理に多くの CPU を利用するので、こちらのパターンとなりました。 メモリが少ないと速度が出ておらず、メモリを増やすに連れて速度も改善しており、コストの増加も緩やかです。ただし、今回はプログラムがマルチスレッド・マルチプロセス対応はしていないため、コア数が増える 1770MB 以降は大きな速度改善は見られず、コストが上がる結果となっています。 よって、このケースではコストだけを考えたら低メモリが有利ですが、速度も考えたら 1024MB ~ 1770MB あたりにすることを検討すると良さそうです。
まとめ
処方箋プレビューに透かしを入れることになった背景、実現方法を紹介いたしました。
- 透かし処理はアルファブレンド処理を実施することで実現でき、PDF については透かし用のレイヤーを重ねることで実現できます。
- S3 Object Lambda を利用することで、追加のサーバー構築をすることなく、S3 のファイルに対してフィルタリングをかけることができます。
- Lambda のパフォーマンス・チューニングについては、AWS Lambda Power Tuning を利用して可視化できます。外部 API などネットワーク依存(Network-intensive)ではなく、CPU 処理が中心(CPU-intensive)になると、コスパよく利用することができます。
S3 のファイルに対してなんらかの処理を実施したいと考えてる方は、一度 S3 Object Lambda の利用を検討してみてはいかがでしょうか?
また、Lambda のチューニングポイントは色々ありますが、AWS Summit Online で紹介された以下のセッションがおすすめなので、もっと深くチューニングしたい方は是非ご覧ください!
さいごに
メドレーでは、医療分野の社会課題を IT にて解決するために日々邁進しております。医療という分野においては、機微な情報を扱ったり診療という大事な業務を止めないよう、可用性、パフォーマンス、セキュリティともに高いサービスレベルを求められます。興味がある方は是非ご連絡ください。
- 元画像に直接文字や図形などを描画することももちろん可能ですが、透かしを画像データとしてデザイン、管理したいこともあり、今回はアルファブレンドを選択した。アルファブレンド - wikipedia
- pdfcpu API document: watermark example
- ただし最初の 60 億 GB 秒/月。昨今は円安なのでドル建てで利用しているインフラコストも上がっており、USD 表記だとコストが変わらないようにみえるので要注意。AWS Lambda 料金
- この UI は AWS Lambda Power Tuning UI というツールで別途提供されており、 `lambda-power-tuning.show` ドメインでアクセスできるので、自前で用意する必要はない。URL は `https://lambda-power-tuning.show/#AAEAAgADAAQ=;xKDwRUqaakU5+RtFcXLqRA==;UqkHOPJCBDjA6AM46DAEOA==;AAEAAgADAAQ=;aYP6RdyeeUUQKiVFuC76RA==;ZDoNOJy3DDiJrQs4zhENOA==;100;50` のような形式でデータが URL のパスパラメータにエンコードされている。
- この他にも `strategy` など様々なパラメータがある: aws-lambda-power-tuning: README-INPUT-OUTPUT.md