【設計の振り返り】AWSサービスを比較検討して決めた画像加工機能のアーキテクチャ

こんにちは、広告事業本部のアプリケーションエンジニアをしているるりとです。
最近は新米工場長として Factorio の世界で工場建設に日々邁進しています!

今回は、AWS環境下で画像加工機能の設計部分を担当したので、その内容を振り返りながら紹介できればと思います!

背景

先ほど紹介した画像加工機能は、社内向けに提供しているWebサービス(以下社内サービス)に対して組み込みました。
社内サービスでは、ユーザーが画像をアップロードする機能を以下のような構成で提供しています。
ユーザーがアップロードした画像はサーバーを経由してS3に保存され、S3に画像を配置したタイミングで EventBridge + Lambda を用いたサムネイル画像の生成処理も行っています。

既存の画像アップロード機能の構成

このような機能に対して、新しく画像を加工する処理を組み込む必要が出てきました。
また、開発当時は下記のような背景もありました。

  • 画像加工処理(Go言語製)自体はすでに完成していたこと。
  • ファイルのアップロード方法を改修する別プロジェクトが並行で進んでいたこと。
  • 加工処理はLambda上で動作させることがほぼ決定していたこと。

このような状況の中で、画像の加工処理をどのタイミングで実行するか、またどのような仕組みで実現するかの検討・設計を担当しました。

設計する上でのポイント

今回の画像加工機能の設計では、大きく分けて以下の2つのポイントを検討する必要がありました。

  1. いつ加工処理を動かすか?(フローの決定)
  2. どんな仕組みで実現するか?(アーキテクチャの選定)

これからは各ポイント単位で、採用した内容や選定理由などを振り返りながら紹介していきます。

フローの決定

フローの決定ですが、どのタイミングで画像を加工するかは割と自由に設定できました。
というのも、既存のファイルアップロードのフローは以下の画像のようになっており、それぞれの処理のタイミングで画像の加工処理を挟むことが可能でした。 そのような中で、どのようなサービスを用いてどうやって画像の加工を実現するのかを決めるのは難しいです。
なので、まずは画像の加工タイミングをある程度決めることで、選択肢の幅を狭めることにしました。

画像の加工フロー

また案を考える上で1つ制限事項がありました。
それは今回追加する画像の加工を終えた後に必ず 6' ~ 7' のサムネイル生成処理を実行する必要があるということです。
画像の加工前にサムネイルを生成してしまうと、加工後の画像に対してサムネイルの生成がされず元の画像とサムネイルの内容が異なってしまうため、対応が必要でした。

それぞれのポイントで画像の加工ができるため、単純に考えると案が7つも出来てしまいます。 そのため、以下の観点を元に案を絞り込みました。

  • 観点1 ユーザー体験が損なわれないこと
  • 観点2 費用を抑えられること
  • 観点3 実装コストを抑えられること

ユーザー体験についての補足です。
1 ~ 6 (ユーザーがファイルをアップロードしてから、結果が表示されるまで)の処理の間ユーザー側では読み込み中であるUIを出すようにしています。
1 ~ 6 の間に同期的に画像を加工する処理を入れ込む場合は、一連の処理が完了するまでの時間が長くなってしまい、ユーザー体験が損なわれてしまいます。
なのでできるだけユーザーがファイルを選択してから、結果の反映までの時間を伸ばさないようにする必要がありました。

上記の条件の元、画像の加工タイミングを以下の2つに絞りました。
選定理由はプロダクト固有の話も含まれるため割愛させていただきます。

  • 4 (サーバー側でデータ保存を行う)が完了した後
  • 7' (サムネイル生成を行う)が完了した後

また 4 の処理(サーバー側でデータ保存を行う)が完了した後のタイミングで処理を行う案を本命とし、
アーキテクチャの選定部分で致命的な問題が生じた場合は 7' の処理(サムネイル生成を行う)が完了した後に画像を加工する方法を検討しました。

7' の処理(サムネイル生成を行う)が完了した後に画像を加工するケースはサムネイル生成処理を2回行う必要があるため、観点2の費用を抑えることはできません。 しかしながら既存の処理を拡張することで対応できるため、観点3の実装コストは大きく抑えることができるため候補に残していました。

結果的にアーキテクチャの決定においては 4 の処理(サーバー側でデータ保存を行う)が完了した後に画像を加工することは問題なかったため、
次点の案を採用することはありませんでした。

アーキテクチャの決定

前項にて画像の加工タイミングがある程度決まったため、次にどうやって画像の加工を実現するかを検討しました。
また非同期的に画像を加工することが前提となるため、非同期処理を実現するためのAWSサービスを中心に検討しました。
候補として出ていたのは以下の4つです。
当初の設計タイミングでは案1~3までの検討を行っていましたが、後にファンアウト構成の案も検討することになりました。

  1. SQS + Lambda
  2. EventBridge + Lambda
  3. API Gateway + Lambda
  4. (ファンアウト構成)SNS + SQS + Lambda

またフローの選定と同じく案を決める上での観点も事前に整理しました。

  • 観点1 拡張性を担保できること
  • 観点2 シンプルな構成であること
  • 観点3 実装コストを抑えられること

上記の観点を元に、それぞれの案のメリット・デメリットを整理しました。 整理した内容を踏まえた上で、 以下の理由からファンアウト構成を採用することに決定しました。

  • 画像の加工タイミングを柔軟に変更できることから観点1の拡張性を担保できる
  • 案2の EventBridge + Lambda と違い画像の加工に必要な情報のサイズに制限がないことから観点1の拡張性を担保できる
  • 一次情報を始めとした情報源が豊富に存在するため、観点3の実装コストが膨れ上がる懸念が少ない
  • ファンアウト構成を採用することで、画像加工の開始や終了タイミングで別の処理を追加することが容易になるため、観点1の拡張性を担保できる

案1 SQS + Lambda

SQS + Lambdaの構成

メリット

  • 情報源が豊富に存在する

SQS + Lambda の組み合わせは公式でも紹介されている手法の一種であるため、 実装を進める上での情報源は豊富に存在すると考えていました。
今回設計を担当した私はそこまでAWSのサービスに詳しくなかったため、情報源が豊富に存在することは大きなメリットでした。 ある程度信頼できる一次情報をベースに実装を進めることができるため、観点3の実装コストの軽減に寄与できると考えていました。

  • 画像の加工タイミングを柔軟に変更できる

SQS にメッセージをキューイングすることで画像の加工が行われるため、キューイングのタイミングさえ調整すれば画像の加工タイミングを柔軟に変更できます。
想定している主な使い方としては、ファイルアップロード時にバックエンド側で SQS にキューイングすることで画像を加工することでしたが、バッチ処理を用いた呼び出しや、定期処理での呼び出しなども問題なく対応できます。 このように、画像の加工タイミングを柔軟に変更できる点は観点3の拡張性を担保できることに繋がると考えていました。

デメリット

  • 料金が高くなる懸念が残る

SQS へのメッセージのエンキュー回数は多くても月100万回を下回る想定であるため、 SQS 単体で見た時の料金は低く抑えることができます。 しかしながら、SQS + Lambda では Lambda 側で SQS のメッセージをポーリングする仕様になっています。
Lambda のポーリングの頻度自体は調整可能であり料金を抑えることはできるものの、画像加工処理の実行回数に依存しない部分での料金がかかる点や、Lambdaの稼働数を増やすことで生じる料金の上がり幅の大きさはデメリットとして残りました。

案2 EventBridge + Lambda

EventBridge + Lambdaの構成

メリット

  • 既存のリソースや知見の使い回しが可能

既存のサムネイル生成処理は、 S3 に画像がアップロードされたタイミングで EventBridge を用いてトリガーされるようになっていました。 また既存のサムネイル生成処理で利用するリソースは Terraform を用いてコード化されており、CI/CD のパイプラインも構築されていました。 そのため、既存のリソースをある程度使い回しながら開発を進められる点は、観点3の実装コストを抑えることに寄与できると考えていました。

デメリット

  • 画像の加工作業に必要な情報の受け渡し量に制限がある

EventBridge 経由で Lambda をトリガーする場合、 S3 のメタデータに画像の加工作業に必要な情報を埋め込むことで、 Lambda 側で画像の加工に必要な情報を渡す方法を想定していました。
ただ S3 のメタデータに埋め込める情報量に制限があり、今回利用したいユーザー定義のオブジェクトメタデータには2KBまでしか情報を埋め込むことができません。 設計をしていたタイミングでは、画像の加工に必要な情報量は多くても1.5KB程度であるため実現は可能でした。 しかしながら画像の加工に必要な情報の中にはユーザー側で入力する情報も含まれていたり、今後の機能追加で画像の加工に必要な情報が増える可能性もあったため、 この制限は観点1の拡張性を担保しにくいと判断しデメリットとして残りました。

案3 API Gateway + Lambda

この案は他の案に比べてそこまで案の深掘り・検討は行いませんでした。
理由としては、外部にAPIを公開する必要があり認証やセキュリティの観点での検討が必要になることが挙げられます。 他の案は閉じた環境下でのみ利用するため、上記の内容の検討・実装は実装コストを大きく上げる要因となります。 そのため、今回の画像加工機能の実装においては API Gateway + Lambda の組み合わせは採用しませんでした。

[採用]案4 ファンアウト構成(SNS + SQS + Lambda)

ファンアウト構成

ファンアウト構成を検討することになった経緯の紹介の前に、簡単にファンアウト構成の概要を紹介します。

ファンアウト構成とはAmazon SNS 通知の Amazon SQS キューへのファンアウトと非同期処理 で紹介されている手法です。
SQS に直接メッセージを送信する方法では、送信元と SQS が 1:1 の関係になりますが、ファンアウト構成では SNS を介して SQS に送信するため、送信元と SQS の関係が 1:N になります。
ファンアウト構成を採用することで、 SNS のトピックに対して新しいサクブスクライバーを追加するだけで、処理の拡張が可能になります。

案の紹介でも触れましたが、実は当初はファンアウト構成を考えていませんでした。 実際に開発を進める上で、本番・検証用環境(AWS上に作成)と開発環境(開発PC上に作成)の間で差分が出てしまう問題が生じており、解決の過程でファンアウト構成を採用することにしました。
上記問題を解消するためにファンアウト構成の検討が始まりましたが、蓋を開けてみると従来の案に比べてファンアウト構成を採用することで、観点3の拡張性の担保ができることが判明しました。
SNS を追加するため工数は増えるものの拡張性を担保できるという点は大きなメリットであるため、結果的にはファンアウト構成を採用することに決定しました。

今振り返るとファンアウト構成を採用したことで、後に進めた追加機能の設計・実装もスムーズに進めることができたため、結果的には良い選択だったと感じています。

振り返ってみて

ということで、画像加工機能の設計について振り返ってみました。 採用したアーキテクチャや設計についてはひねりのない割とベーシックなものでしたが、シンプルな構成で機能を実現できたのは素直によかったと思います。

今回は、あまり経験のない分野で設計できたため、個人としては得られることが多かったと感じています。
やりたいことをベースに設計内容を分解した上で、丁寧に案の作成・比較を行い実装を進めていくという内容は、どのプロジェクトでも行うことだと思います。
そういった基本的な進め方・設計方法をある程度の規模感の元で経験できたことは、今後の自分の成長に繋がると感じています。

最後に、設計や実装を私に任せてくれたチームには感謝の気持ちでいっぱいです。
(後は工数の見積もりが甘く、うまく機能開発の進行できなかったのは反省点です。。。)
今回の経験を元に次の開発ではより良い設計・実装を行えるように頑張りたいと思います!