Lobiの画像変換サーバーをImageFlux+Lambda@Edgeで置き換えたはなし

SREチームの長田です。 先日Lobiの画像変換サーバーをImageFluxに移行したので、その過程を紹介します。

画像変換って?

Lobiはチャットを主軸としたコミュニティサービスです。 ユーザーはチャットメッセージに画像を添付することができます。 また、ユーザーアイコンやチャットグループの壁紙などもスマホやPCからアップロードして設定することができます。

アップロードされた画像は利用箇所に応じて適切なサイズ・フォーマットに変換する必要があります。 投稿画像のプレビューでは表示速度と通信量削減を優先して解像度低めの画像を、 拡大表示する場合はオリジナルサイズの画像を表示します。

複数サイズの画像を用意する手段として、Lobiでは内製画像変換アプリケーションであるmagcian1を運用していました。 Amazon EC2上で動作するNodeJS製のアプリケーションで、AWS CloudFrontのOriginとして設定していました。 クライアントからのリクエストを受け取り、URL中のパラメータを元に、 Amazon S3に保存されているオリジナル画像を適切なサイズ・フォーマットに変換して返します。

グループアイコンのリサイズパターン例:

48x48 100x100 144x144
https://assets.nakamap.com/img/grp/894691995e8bf119d1902178df2ccaec696d52c0_48.jpg https://assets.nakamap.com/img/grp/894691995e8bf119d1902178df2ccaec696d52c0_48.jpg https://assets.nakamap.com/img/grp/894691995e8bf119d1902178df2ccaec696d52c0_48.jpg

オンデマンドに画像変換するのでリクエストが発生した場合に変換処理のみを行えば良いというメリットがあります。 予めパターンごとに画像変換して静的な画像を用意する場合、用意したものの使用されない画像があったり、 変換後の画像の数だけストレージを圧迫するという問題があります。

変換処理の分レスポンスが遅れてしまうというデメリットがありますが、 一度AWS CloudFrontにキャッシュされてしまえば次回以降は高速にレスポンスを返すことができます。

f:id:handlename:20190722163441p:plain
magician時代の構成図

課題

magcianを廃止するモチベーションとして、以下の課題がありました。

古い

magicianがプロダクション環境に投入されたのは2012年でした。 そこから7年が経過し、様々な人が手を入れ、特に変換の入出力を定義した設定ファイルが魔境化してしまいました。

当時はサーバーサイドエンジニアチーム内でも使用人口が多かったNodeJS製なのですが、 年月経過とともにサーバーサイドでのNodeJS利用に精通したエンジニアがいなくなってしまいました。 造り自体はシンプルだったため、必要に応じて手を加えてはいたのですが、 コア機能である画像変換処理を担うImageMagickのNodeJSバインディングであるnode-imagemagick-nativeを メンテナンスするまでには至りませんでした。

ImageMagick自体もその多機能さゆえに、安全に運用するためには努力が必要でした。

qiita.com

NodeJS自体のバージョンも古く、最近のバージョンまであげようとすると使用できない依存パッケージがあり、 そのパッケージを別のものに置き換えるとまた別のものが動かなくなり・・・、 と影響範囲が広くなってしまい、結局は「動いているから現状維持で」という結論に落ち着いてしまっていました。

アプリケーション数が多い

magicianはいわゆるマイクロサービスです。 当時は利用可能な製品がなかったために自前で作成したアプリケーションなのですが、 いまでは同等の機能を備えたManegedなサービスが利用できます。 自前アプリにメリットが無くなっていたので、そろそろmagcianを廃止したいという声が前々から上がっていました。

Amazon S3 SigV4に対応していない

決定的だったのはこちら。 magcianは2019年6月末に廃止がアナウンスされたAmazon S3 SigV22を利用していました。

dev.classmethod.jp

docs.aws.amazon.com

Amazon S3との通信部分をアップグレードしなければ機能が停止する・・・! アップグレードのコストを払って延命するくらいならManaged Serviceに移行したい・・・!

ということで移行することにしました。

ImageFlux

移行先としてさくらインターネット社が提供しているImageFluxを選択しました。

www.sakura.ad.jp

magcianと同等の機能が利用できることと、 すでにLobi Tournamentで利用実績があったことが選択理由となりました。

vs.lobi.co

移行に必要な要素

リクエストパラメータの変換

magcianもImageFluxも、URL中のパラメータを元に画像を変換して返すことは共通していますが、 パラメータのフォーマットが異なります。 URL自体をImageFluxに寄せてしまうことができればシンプルなのですが、 Lobiのクライアントアプリは画像のURL自体をキャッシュするため、 URLを完全に移行することは難しいと判断しました。

そこで、CloudFrontのImageFlux用Distributionで、 Origin requestとしてLambda@Edgeを設定することにしました。 Lambda Functionがリクエストパラメータをmagcian用からImageFlux用に変換することで、 URLを変えることなくImageFluxを利用することができます。

オリジナル画像の取得先

magcianはAmazon S3からオリジナル画像を取得するアプリケーションです。 ImageFluxもS3もOriginとしてAmazon S3を指定できるので、移行にあたって実作業は発生しませんでした。

クライアントからの参照先URL切り替え

いきなり全リクエストをImageFluxに切り替えるのは、Managed Serviceとはいえ怖いものです。 今回はLambda@Edgeによるリクエストパラメータの変換も行っているため、 ImageFluxには初めは少量のリクエストを流して様子を見ることにしました。

DNSとしてAmazon Route 53を利用していたため、 その機能であるWeigted Routingを使って、 リクエストの一部をImageFluxをOriginとしたCloudFront Distributionに向けることにしました。 (この方法は実はうまくいかないのですが、それについては後述します)

docs.aws.amazon.com

監視

Lobiではサービスの監視にはてな社が提供するMackerelを利用しています。

mackerel.io

MackerelのAWSインテグレーションではLambda Functionも対応しています。 が、Lambda@EdgeのメトリクスはCloudWatch上に us-east-1.{Lambda Function名} というFunctionNameで保存されているため、 AWSインテグレーションでは収集できないようです。

MackerelとCloudWatch、ふたつのサービスを見なければならない状態は一覧性に欠けます。 そこでcloudwatch-to-mackerelを使ってCloudWatch上のメトリクスをMackerelに送信することにしました。

github.com

CloudWatch から Mackerel にメトリックを送る方法 2019年版 / Mackerel Meetup 13 - Speaker Deck

メトリクスはCloudFrontのエッジロケーションごとに分かれているので、 Mackerelの式グラフを使って全エッジロケーションの合計や平均をグラフ表示しています。

mackerel.io

sum(
  service(production, imageflux_proxy_trigger_orgreq.Invocations.Sum.*)
)

エッジロケーション名部分を * にすることで、すべてのロケーションを対象にしています。

つまづきポイント

AWS CloudFrontとAmazon Route 53のWeighted Routing

「移行に必要な要素」で書いたように、計画当初はWeighted Routingのweightを調整して、 まずは少量のリクエストをImageFlux用のCloudFront Distributionに流す予定でした。

ところが、CloudFrontには以下のような制限が設けられていました。

2 つのディストリビューションで代替ドメイン名が重複している場合、 CloudFront は、DNS レコードが指しているディストリビューションに関係なく、 より具体的な名前が一致しているディストリビューションにリクエストを送信します。

docs.aws.amazon.com

今回の場合、CloudFront Distributionに設定するCNAMEsとして、

  • もともとの画像配信用のドメインである assets.nakamap.com 3 をmagcian用のDistributionに
  • *.nakamap.com をImageFlux用のDistributionに

それぞれ設定していました。 ImageFlux用のDistributionにワイルドカードが指定してあるのは、 Distribution間で重複できないという制限があるためです。

Route 53でWeightの設定をして、一部のリクエストを流して・・・流れて・・・こない? 上記の制限のため、より具体的なドメイン名である assets.nakamap.com が設定されたmagician用のDistributionに すべてのリクエストが流れてしまっていたわけです。

代替策として、

  • 切り替え中はCloudFrontを通さずmagcianが直接リクエストを受ける
  • Weighted Routingはmagcianがぶら下がっているロードバランサーとImageFlux用Distribution間で行う

とういう方法をとりました。 magicianをを動かすAmazon EC2インスタンスのサイズを平時よりも大きくした上で数を減らし、 magicianの前段であるnginxのproxy_cacheにヒットしやすい状態にしました。 キャッシュヒット率を上げ変換処理の実行を抑えることでCloudFrontなしでもリクエストをさばくことができました。

・・・あとになって見返すと、「CNAMEsは重複できない」という制限のすぐ後に「より具体的な〜」という記述があるんですよね。 ドキュメントはよく読めというはなしです。

クライアントキャッシュ関連のヘッダーがつかない

ImageFluxはS3 Objectに設定されたヘッダーをそのまま返します。 S3 Objectとして保存されているほとんどの画像ファイルにはCache-ControlおよびExpiresヘッダーが設定されていたのですが、 ある種類の画像にはこのヘッダーがセットされていませんでした。

もともとはnginxでヘッダーをセットしていたようです。 対象画像ファイル数は一括処理でヘッダーをセットできる量ではなかったので、 Origin ResponseにもLambda@Edgeを設定し、そこでクライアントキャッシュ関連のヘッダーをセットすることにしました。

Lambda@EdgeによるLambda FunctionのInvokeにも費用は発生するため、 将来的には大量にある対象画像ファイルにヘッダーをセットし、Origin Responseも廃止する予定です。

TLSv1.0

現在、CloudFrontで新規Distributionを作成すると、セキュリティポリシーのデフォルト値は TLSv1.1_2016 なのですが、 このポリシーはTLSv1.0をサポートしていません。

docs.aws.amazon.com

執筆時点でLobiのAndroidサポートバージョンは4.2以上としていました。 AndroidがTLSv1.1および1.2に対応したのはバージョン5.0以降です。 このため、セキュリティポリシーはTLSv1.0をサポートした TLSv1_2016 に変更する必要がありました。

SNI

magcian用のCloudFront DistributionではSNIを無効にしていました。 当時のクライアントはSNIに対応していなかったためです。 CloudFrontでSNIを無効にしている場合、Distributionごとに毎月$600のコストがかかります。

aws.amazon.com

最近のクライアントはSNIに対応しているので、ImageFlux用のDistributionではSNIを有効にしました。 Lobiのクライアントアプリでは事前に画像の表示に問題がないことを確認し、実際に問題は発生しませんでした。

予期せぬ影響があったのが運営チームで使用している画像監視ツールでした。 使用しているクライアントライブラリが古く、SNIに対応していないものだったため、 一時的に画像監視がストップした状態になってしまいました。 クライアントライブラリをアップグレードすることで間もなく復旧しました。

複雑な設定ファイル

「魔境」と表現した設定ファイルですが、これもLambda@Edge用に移植する際も一筋縄ではいきませんでした。 完全にローカルトークになってしまうので詳しくは書きませんが、 設定ファイル内の別の場所を参照できるエイリアスがあったり、一部のパスはnginxでreverse proxyしていたり・・・。

独自プロダクトにはありがちな話で設定ファイルの仕様も存在しなかったため、 設定の種類ごとに実際のリクエストを1000パターンほど送ってみて動作確認→だめだったら原因調査して直す という作業を何度か行うことになりました。

移行してどうだったか

magcianからImageFluxに移行することで、自分たちで運用しなければならないアプリケーションをひとつ減らすことができました。 Managed Serviceを利用することでサービス運用の人的コストを下げることができ、満足度が高いです。 ImageFluxの利用料も、移行前のサーバーリソースと比べて大差ないレベルに抑えることができました。

f:id:handlename:20190722163503p:plain
ImageFlux+Lambda@Edgeの構成図

やりのこしたこともあります。

問題として挙げていた「設定ファイルの魔境化」については、magician時代のURLを変換しなければならない都合上ほぼそのままのかたちで Lambda@Edge用のLambda Function内に残っています。 これをリファクタリングするためにはクライアントアプリのリクエストを変更しなければなりません。

また、Lambda@Edgeでは現状NodeJSしか使えません。 リクエストパラメータを変換するだけのコードになったとはいえ、普段から利用していない言語で書かれたコードは手に余るものです。 Lambda@EdgeでもGoが使えるようにならないかな・・・。

おわり

今回の対象は「画像を変換する」というシンプルなシステムでしたが、複数のつまづきポイントがありました。 運用が長期化していると思わぬ影響があるものです。 レガシーシステムを刷新する場合は慎重に。


  1. 命名の理由はImageMagickを使っているからなんだとか。

  2. 結局既存Amazon S3 BucketのSigV2の廃止なくなったんですけどね https://aws.amazon.com/jp/blogs/news/amazon-s3-update-sigv2-deprecation-period-extended-modified/

  3. nakamap はLobiの旧サービス名です https://www.kayac.com/news/2013/05/lobi_release