Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

WebP変換 & 画像キャッシュサービスをサーバレスで構築する - Feed re:Architect vol.1 -

Eight事業部 Platform Unit / Engineering Manager の 藤井洋太郎(yotaro) です。

前回、以下のRuby on Lambdaを使った画像処理基盤の記事を書かせていただきましたが、

今回の記事では少し前に、Eightフィード機能の高速化の際に行った、「画像フォーマット変換、リサイズ、キャッシュサービスをサーバーレスで構築した話」をしたいと思います。

Eightフィード

Eightのメイン機能として、「ニュースフィード」があります。
この機能は、これまでに名刺交換をした「人」や「企業」に関連するニュースや更新情報を届けるものです。例えば、

  • 役職変更、転勤などの人事異動情報
  • 企業のプレスリリースなどの最新ニュース
  • ユーザや企業による最新動向

など「ビジネスに必要な情報」が各ユーザにパーソナライズされた形で届きます。

f:id:yotaro-fujii:20190605170450p:plain:w150:left f:id:yotaro-fujii:20190605171048p:plain:w150

サクサク動かない。原因はOGP画像!?

しかし、そんなフィード機能にある課題を抱えていました。それはフィード投稿に含まれる「OGP画像」の読み込みが遅く、フィードのスクロールがカクカクしたり、画像が読み込まれるまで動かないという課題でした。

調査をしたところ、OGP画像として設定されている画像のサイズがかなり大きいケースがあることが大きな原因だとわかりました。 (なんと、サイトによっては数MBの画像が指定されていることも!)

そこで、私達のチームは画像のリサイズやフォーマット変換、キャッシングを行うことでユーザ体験の向上を図りました。

サーバレスなサービスの構築

画像圧縮やキャッシュ処理を行う機能は、Eight本サービスから切り出した、サーバーレス環境で構築することにしました。 環境は以下です。

  1. 画像フォーマット: JPEG, WebP
  2. AWS: CloudFront, API, AWS Lambda, S3
  3. 言語: Python3.6
  4. フレームワーク: Serverless Framework
  5. 開発環境: Docker
  6. CI/CD: CircleCI

Eightの開発は Ruby on Rails がメインですが、当時RubyはLambda非対応だったこともあり言語は Python を利用しました。
また、フレームワークにはAWS Lambdaだけでなく周辺環境の構築が容易でプラグインも豊富なServerless Frameworkを採用しています。

WebPフォーマットの採用

また画像変換する際に最も重要視したのは WebP画像 に対応することでした。
WebPとはGoogleが開発した画像フォーマットで、同品質のJPEGと比べて約25-34%ほどファイルサイズを小さくできるとされています。
対応ブラウザはこちらから確認できます。Safariはまだ対応していませんが、その他のモダンブラウザでは扱うことができます。

今回は、WebPフォーマット変換とリサイズを行い最適化するので、かなりのサイズ削減が期待できます🚀

構成図

早速ですが、全体の構成図は以下のような形になっています。 f:id:yotaro-fujii:20190603023535p:plain

画像のキャッシュはCloudFrontを利用し、キャッシュが存在していなければAPI Gateway -> Lambda と処理が渡されオリジナル画像を取得 & フォーマット変換を行う流れです。

構成自体は一般的な形と言えるかもしれませんが、開発するにあたり工夫した点をここから紹介したいと思います。

Python3.6 で WebP変換

PythonでWebPを利用するには主に2つの方法があるかと思われます。

  • cwebp をOSコマンドから叩いて変換
  • Pillow(PIL)などの画像処理ライブラリを利用
    • https://python-pillow.org/
    • Pillowは開発中止となったPILからフォークされ開発されているpythonでは有名な画像処理ライブラリ
    • 様々な画像操作が可能で、対応フォーマットも豊富

変換のみであればcwebpでも良かったのですが、今後画像ファイルを操作することも考慮し、後者のPillowを利用することにしました。

Pillowを使った画像のWebP変換は、まずPillowをインストールし、

$ pip install Pillow

以下のようにすることでWebPに変換できます。

from PIL import Image

im= Image.open('jpeg_file_path')
im.save('webp_file_path', 'webp')

変換時に品質の調整などを行いたい場合は、save メソッドの引数にオプションを渡すことで調整できます。 渡せるオプションはフォーマットによって異なるので、Pillowの公式ドキュメントを参照すると良いでしょう。

と、ここまでは単純そうに見えますが、Pillowは所謂 pure Python なライブラリではないため、お手元の環境次第ではWebP変換に必要なライブラリが見つからずエラーが出ることが多々あります。 つまり、手元で動作してもLambdaの動くAmazon Linux上でも確実に動作するとは言い切れません。

そこで、開発環境やビルド/パッケージ環境もLambdaと同等の環境で実行していくことがプラクティスとなってきます。

Docker環境の構築

Lambda実行環境を擬似的に再現できる lambci/docker-lambda を使います。 lambdaでサポートされている言語、バージョンでタグが打たれています。 今回は python3.6 を利用するため、 lambci/lambda:build-python3.6 イメージを使っていきます。

$ docker run -it lambci/lambda:build-python3.6 /bin/sh
$ pip install Pillow

Pillowがインストールできた状態で、WebPが扱えるか試してみましょう。 おそらく、問題なく動作するはずです。提供されているLambda環境では、ライブラリの追加や環境変数等の設定も不要そうです。

Lambdaにデプロイする

Lambdaにデプロイするためには、外部ライブラリも含めた形にしなければいけません。そこでライブラリをinstallする際は

# Dockerコンテナ上で作業
$ pip install Pillow -t ./
$ zip -r zip_file ./*

などとし、作業ディレクトリ配下に展開した上で、zip化 & アップロードを行うと良いでしょう。

Serverless Frameworkの設定

Docker上で操作する前提でここまで書きましたが、今回の構成は Lambda だけでなく、 API Gateway との接続等も行う必要があります。 これらの構成まで手動管理というのはさすがに再現性がないのでフレームワークを使って管理していきましょう。

今回は Serverless Framework を使って管理していきます。

API Gateway と Lambda の接続

npm install serverless でインストールし、作業ディレクトリ直下でプロジェクト作成(serverless create)をします。
その後 serverless.yml を以下のように設定することで Lambda と API Gateway を管理できます。

provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1

functions:
  get_ogp_image:  # 関数名
    handler: functions/ogp_images.get  # 関数の場所
    description: 渡された画像urlからオリジナル画像を取得し、フォーマット変換やリサイズを行う  # 関数説明
    events:
      - http:               # API Gateway の設定
          path: ogp_images  # APIパス
          method: get       # HTTPメソッド

Lambda と API Gateway の連携自体は上記のようにとても簡単ですが、実は今回の構成はこれではうまく動きません。 今回は画像バイナリを返すAPI(サービス)となるので、バイナリを扱うための特別な設定が必要となってきます。

f:id:yotaro-fujii:20190605155309p:plain
Lambdaから渡されるBase64テキストをバイナリペイロードに変換

CONVERT_TO_BINARY

API Gateway の公式ドキュメント には、

IntegrationResponse リソースの contentHandling プロパティを CONVERT_TO_BINARY に設定して、レスポンスペイロードが Base64 でエンコードされた文字列からそのバイナリ BLOB に変換されるようにする

とあるため、それに従ってAWS Console上から設定してみます。

f:id:yotaro-fujii:20190605140510p:plain
Integration Response で Convert to binary を指定

設定反映後 API Gateway 上でテスト実行してみると、以下のようにバイナリ形式でレスポンスが返ることがわかります。

f:id:yotaro-fujii:20190605140900p:plain
WebPかつバイナリでレスポンスされていることがわかる

ここで設定した、バイナリ変換オプションですが Serverless Framework ではデフォルトでは対応していません(たぶん)。
そこで、プラグインを使って実現していきましょう。

serverless-apigw-binary プラグイン

serverless-apigw-binary というAPI Gatewayのバイナリオプションを有効化するプラグインを使いました。

npmパッケージとして提供されているため、 npm install --save-dev serverless-apigw-binary といった形で先にインストールしておきましょう。

インストール後は serverless.yml に

plugins:
  - serverless-apigwy-binary

...

functions:
  get_ogp_image:
    handler: functions/ogp_images.get
    description: 渡された画像urlからオリジナル画像を取得し、フォーマット変換やリサイズを行う
    events:
      - http:
          path: ogp_images
          method: get
          contentHandling: CONVERT_TO_BINARY # <- 今回追加したい設定

とすれば良いです。この柔軟さやプラグインの豊富さが Serverless Framework の強みですね!

デプロイ

そして最後はデプロイです。

$ serverless deploy

基本のコマンドは上記ですが、stagingとproductionを切り替えたいといったケースもあると思います。 その場合は --stage optionを使うことで設定を切り替えられます。

$ serverless --stage staging
$ serverless --stage production

また、デフォルトでは作業ディレクトリ直下のファイル群をパッケージング化するようになっていますが、テストコードなどテスト・ローカル環境のみで利用するコードもあるでしょう。
このような場合は、Packaginginclude/excludeを設定することで、必要なコードだけデプロイすることが可能となります。

また、詳細の解説はここではしませんが、環境変数の設定やIAM Roleの設定など細かい設定も可能なので、環境に合わせて設定していただくと良いと思います。

その他: TIPS

Docker環境

今回は深く掘り下げませんでしたが、基本的にはすべてDocker環境上に構築しています。 lambci/lambda:build-python3.6 イメージをベースに、serverless framework やserverlessを実行するための node などを含むDocker Imageを独自に作成しています。
そして、コンテナを立ち上げる際に、ローカルの作業ディレクトリをDockerコンテナにマウントさせています。 こうすることのメリットとしては以下があげられます。

  • 実環境でのみ発生するトラブルを極力避けられる。
  • 開発環境を容易に構築することが可能。
  • コーディングなどの作業は、ホスト側で行えるので馴染みのエディタなどを使える。

以前書いたRuby on Lambdaの記事でも同様のことを行っているので参考にしてもらえればと思います。

serverless-python-requirements プラグイン

serverless-python-requirements というプラグインを使うと、requirements.txtに記載したmoduleをデプロイ時にいい感じにパッケージング化してくれます。
また、pure Pythonでないmoduleも以下のようにすることでLambda Docker環境上でクロスコンパイルしてくれます。(すごい!)

custom:
  pythonRequirements:
    dockerizePip: true

ですが、今回は上述したようにすでに開発環境自体をDockerで構築しているため、上記の指定をする必要はありません。
Python開発ではおなじみの requirements.txt でモジュール管理できるのはとても便利です。使わない手は無いでしょう🙌🏻

Lambda Layers

Lambda Layersは当時はまだ無かった機能ですが、Pillowなどのシステム依存なライブラリはLayersに切り出すとより良いかもしれません。

リリース後の反応

そして最後にリリース後の反応です。
画像配信サービスをリリースした後、メインサービスから見ていた画像へのアクセスの向き先をこのサービスに向かせるようにしました。

オリジナル画像のリサイズやフォーマット変換を行ったため、ファイルサイズは大きいもので10〜100分の1になることはわかっていましたが、その効果は想像よりも大きいものでした。

これまでアプリやブラウザで閲覧する際に感じていた、フィード描画時のワンテンポの遅延が明らかに解消されました。

f:id:yotaro-fujii:20190605165618p:plain
弊部長の喜びの声🤣🚀

このように、リリース直後から反響は大きく、フィードバックはもちろん、フィード投稿のインプレッション数ネットワーク使用量も大きく改善されました。

おわりに

今回の記事では、 Eightのコア機能である「フィード」の裏側で動いている、画像処理サービスについて紹介しました。

この対応を機に、Eight内での他の画像処理機構においても WebP フォーマットがファーストチョイスとなり広く採用するきっかけになりました。 こういった細かなパフォーマンス改善がサービス品質を支えていくということを改めて体感する機会となりました。

また、Eightフィード機能は長い歴史がありますが、この半年の間にゼロベースでアーキテクチャを刷新した機構でもあります。 その話は、また次の記事で紹介できればと思います👋🏻

© Sansan, Inc.