こんにちは!SmartHR で基盤開発や横断的な技術的負債の解消を担当しているプロダクトエンジニアの kumaie です。この記事では、Cloud CDN を利用して画像配信フローの見直しを行ったプロジェクトについて紹介します。
従来の画像配信フローと課題 ── アプリケーション経由で配信
私たちが開発している SmartHR では、ユーザーがアップロードした画像を Cloud Storage (以下、GCS) の非公開バケットに保存しています。
これまでは、ユーザーからのリクエストに対して画像の URL をアプリケーションで生成し、画像そのものもアプリケーションを経由して GCS から画像を取得してユーザーに返していました。

このような構成であったのは、以下を意図したためです。
- googleapis.com など、クラウドサービスの URL を隠蔽したい
- ユーザーのセキュリティポリシー上、特定の IP アドレス以外への通信を許可しないことがあるため、アクセス先を固定したい
- 認証されたリクエストのみに画像を返したい
ただ、アプリケーション側では画像のキャッシュを一切行っていなかったため、リクエストのたびに GCS へアクセスする必要があります。
そのため、アプリケーションの負荷が増大したり、レスポンス速度が遅くなるという課題がありました。
最近になって SmartHR に 組織図機能 が追加され、短時間に大量の画像を扱う機会が増えました。また、リクエストの多い年末調整時期のシステム負荷も年々高まっています。これらを踏まえ、改善が必要と判断しました。
新しい画像配信フロー ── Cloud CDN から直接配信
そこで今回、アプリケーションを経由せず GCS から直接画像を配信できる方式に変更することにしました。
Cloud Load Balaincing の URL マップを利用することで、ロードバランサはアプリケーションを経由せずにリクエストを処理し、GCS から直接画像を取得できるようになります。
ですが、GCS のデフォルトのアクセス制御では匿名ユーザーがファイルを直接取得することはできません。公開バケットにすることで可能にはなりますが、誰でもアクセス可能になるのでセキュリティリスクが大きく、許容できません。
また、これまでの構成意図も維持する必要があります。
これらの問題や要件を満たしつつもアプリケーションを経由せずに画像を配信する方式を検討した結果、URL マップによるアプリケーションのバイパスだけでなく Cloud CDN も同時に有効化することにしました。
新しい配信フローは以下の通りです。

ユーザーがアクセスする IP アドレスの固定化を実現するために、画像の URL は引き続きアプリケーションで生成して Cloud Load Balancer がリクエストを受け付けるようにしますが、画像配信処理はアプリケーションを経由せず直接 GCS から返すようにしています。
また、Cloud CDN を有効化することで画像のキャッシュが自動的に行われるようになります。アプリケーションをバイパスするだけでも負荷の問題は軽減されますが、適切なキャッシュの仕組みによってレスポンス速度の問題も解決することができます。
それだけではなく、Cloud CDN を有効化することで 非公開送信元の認証 を構成できるようになります。これにより、GCS には Cloud CDN のサービスアカウントを使ってアクセスすることができ、GCS バケットは非公開のままでセキュアに画像を配信することが可能になります。
加えて、さらにセキュリティを高めるために 署名付き URL も有効化します。これにより、認証されていないユーザーが画像の URL が取得できないようにしつつ、署名付き URL に有効期限も設定しておくことで、URL 自体のセキュリティを高めることが出来ます。
Cloud CDN + Private GCS の設定
前述のフローを実現するために、様々な設定を行います。
以下に設定の要点を記載します。
Cloud Load Balancer、Cloud CDN、GCS の設定
Cloud Load Balancer を介して GCS へのリクエストを処理し、Cloud CDN でキャッシュされるようにします。
Cloud Load Balancer のバックエンドとして GCS バケットを作成します。
素直に作ると GCS バケットが公開されてしまいますので、非公開にするのを忘れないようにしましょう。
また、同時に Cloud CDN も有効化しておきます。
参考: Cloud Storage バケットを使用して従来のアプリケーション ロードバランサを設定する
さらに、署名付き URL を発行するために署名鍵の追加も行います。
署名鍵を追加したタイミングで、Cloud CDN が GCS にアクセスするためのサービスアカウントが作られます。当該アカウントだけが非公開 GCS バケットにアクセスできるようにするために、roles/storage.objectViewer 権限を付与します。
また、signed-url-cache-max-age フラグによってキャッシュ期間を設定できますが、これは署名付き URL の有効期間と揃えておきましょう。
参考: 署名付き URL を使用する
参考: 非公開送信元の認証
署名付き URL の生成
最後に、署名付き URL をアプリケーションで発行します。
以下は実際のプロダクトコードではありませんが、それに近い実装例です。
def signed_url(original_image_url) key = ENV.fetch("SIGNED_KEY_VALUE") key_name = ENV.fetch("SIGNED_KEY_NAME") expiration = Integer(ENV.fetch("URL_EXPIRATION")) domain_name = ENV.fetch("DOMAIN_NAME") decoded_key = Base64.urlsafe_decode64 key # ロードバランサ経由で画像を取得するため URL を加工する # デフォルトでは、 GCS のパスは http://storage.googleapis.com/bucket-name/object-name となっている # 後で URL を加工するために object-name の部分を取得する path = original_image_url.path.delete_prefix("bucket-name") query = { Expires: Time.current.since(expiration).to_i, KeyName: key_name, }.to_query # ロードバランサ経由で画像にアクセスされるように、自前のドメインを指定する base_url = URI::HTTPS.build(host: domain_name, path:, query:).to_s signature = OpenSSL::HMAC.digest("SHA1", decoded_key, base_url) encoded_signature = Base64.urlsafe_encode64(signature) "#{base_url}&Signature=#{encoded_signature}" end
参考: 署名付き URL を使用する
導入の効果 ── 速度向上、負荷軽減
これらの変更により、以下の改善が実現しました。
アプリケーションの負荷軽減
画像へのリクエストがアプリケーションで処理されなくなったことでサービス全体のトラフィックが大きく削減されました。
以下は、本対応をリリースした前後の数日間における、サービス全体のリクエスト数を示したグラフです。リクエスト量には日ごとの変動があるため単純な比較は難しいですが、リリース前後でリクエスト数のピークが明らかに低くなっていることが分かります。
なお、SmartHR はビジネスタイムにリクエストが集中する特性があり、夜間や休日はリクエストが少なくなります。そのため、グラフの谷や特に低い山の部分は、考慮せずに見てください。

トラフィックが削減されたことにより、アプリケーションサーバの CPU 負荷もリリース前後で約 5% ほど低減しています。
レスポンス速度の向上
また、Cloud CDN のキャッシュを活用できるようになったことで、手間をかけることなくより高速に画像を配信できるようになりました。
キャッシュ期間やキャッシュヒット率によって差がでるため一概に比較したり参考にしてもらうことは難しいのですが、SmartHR では 6%〜17% の速度向上を実現しています。
このような成果により安定したシステム運用が可能になり、画像リクエストに限らずシステムパフォーマンスも安定・向上してユーザー体験の改善に繋がっています。
まとめ
GCS から画像を直接配信することで、アプリケーションの負荷を削減し、安定化させることでより快適なユーザー体験を実現できました。
特に、Cloud CDN の導入に伴って非公開送信元の認証や署名付き URL を構成したことで、GCS バケットの非公開設定を維持しつつ、パフォーマンスを向上させることができたのがポイントです。
この方式は大量の画像を扱うサービスで特に効果的です。そのため、同様の課題を抱えている場合にはぜひ参考にしてみてください。
We Are Hiring!
私はバックエンドエンジニアという立場ではあるのですが、今回はインフラ中心の対応を行いました。
SmartHR では今回のように役割にとらわれず幅広く業務に携わることができます。
少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!