はじめに
はじめまして、キカガクでプロダクトマネージャーをしている丸山です。
今回は私がソフトウェアエンジニアをしていた頃に、キカガクのサービス全体に CDN を導入した取り組みについて書きたいと思います!
背景と取り組みの内容
キカガクでは以前からアプリケーション全体の CDN として Cloudflare を採用していましたが、ストレージから取得しているコンテンツや画像が多いページの表示速度の遅さに関するお問い合わせが増えてきていました。
そこで今回は、以下の画像のようなe-ラーニングのコンテンツおよびプロダクト全体の画像にCDN を導入し、キャッシュを管理することで、表示速度の改善を目指します。
コンテンツ
利用したサービスについて、まずコンテンツのパフォーマンス向上には Cloud CDN を採用しました。元々コンテンツは HTML 形式のファイルで Google Cloud Storage に保存されており、それをフロントエンドから Firebase SDK で取得していました。
これを Cloud CDN のキャッシュ機能を利用してエッジキャッシュとブラウザキャッシュを設定することでパフォーマンスの向上を実現しました。また、Cloud CDN の署名付き Cookie 機能を利用することで、署名付き Cookie を発行してコンテンツへのアクセス権限のあるユーザーのみアクセスを許可することでセキュリティを強化しました。
フロントエンドには Next.js を採用しているため、Cookie の発行は API Routes を利用してサーバーサイドでおこなっています。
// 署名付き Cookie を発行 const expiresOfUnix = new Date().setDate(date.getDate() + 1); const decodedKeyBytes = Buffer.from(key, 'base64'); const urlPrefixEncoded = Buffer.from(urlPrefix) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_'); const input = `URLPrefix=${urlPrefixEncoded}:Expires=${expiresOfUnix}:KeyName=${keyName}`; const signature = crypto .createHmac('sha1', decodedKeyBytes) .update(input) .digest('base64') .replace(/\+/g, '-') .replace(/\//g, '_'); const signedValue = `${input}:Signature=${signature}`; res.setHeader( 'set-cookie', `Cloud-CDN-Cookie=${signedValue}; path=/; secure=true; httpOnly=true; sameSite=Lax`, );
この Cookie をヘッダーに付与して Cloud CDN にコンテンツをリクエストし、署名付きCookie の正当性が確認できたらコンテンツが返却されます。コンテンツ返却までの流れは以下のとおりです。
画像
次に、画像のパフォーマンス向上のために、Cloudflare Images を採用しました。
Cloudflare Images は、画像の保管、リサイズ、最適化など、画像の管理と配信に適した機能を提供しています。基本的には、画像のアップロードを完了すれば、画像配信に必要な機能が全て利用できるため、非常に便利です。
Cloudflare Images からの画像取得は、Next.js の Image コンポーネントの Custom Loader 機能を利用し、画像のリサイズ処理や Polish 処理を Cloudflare のエッジに任せることで、フロントエンドの処理負担を軽減しました。
import type { ImageLoader } from 'next/image'; /** * Cloudflare ImagesのLoader * Next/Imageコンポーネントにloaderとして組み込むことで、cloudflare Imagesから画像を取得する * https://developers.cloudflare.com/images/image-resizing/integration-with-frameworks/ */ export const cloudflareLoader: ImageLoader = ({ src, width, quality }) => { const params: string[] = []; // 画質の指定がなければ性能を優先して 90% で返す quality ? params.push(`quality=${quality}`) : params.push('quality=90'); if (width) { params.push(`width=${width}`); } const paramsString = params.join('&'); return `${process.env.NEXT_PUBLIC_CLOUDFLARE_IMAGES_URL}${src}?${paramsString}`; };
import Image from 'next/image'; <Image loader={cloudflareLoader} src={`${IMAGE_ROOT_PATH}/sample.svg`} width={360} height={260} />
また、キカガクでは Chakra UI を UI フレームワークとして採用しており、Chakra UI の Image コンポーネントも使用しています。このコンポーネントには Custom Loader 機能が存在しなかったため、やむを得ずデフォルトサイズの画像を取得し、フロントエンド側でリサイズを行いました。
<Image src={`${process.env.NEXT_PUBLIC_CLOUDFLARE_IMAGES_URL}/sample.svg`} width={'420px'} height={{ base: '240px', lg: '330px' }} objectFit='contain' />
Cloudflare Images からの画像配信に関しては、セキュリティを強化する目的で Cloudflare Workers を利用し、internal というパス名が設定されている画像については、特定の URL 以外からのアクセスが Cloudflare のエッジで拒否されるように設定しています。
export default { async fetch(request) { // whiteListに記載したURL以外からアクセスされた場合アクセスを拒否する const referer = request.headers.get('Referer') let url = new URL(request.url) const whitelist = [ // 画像へのアクセスを許可するURL ]; if ( (!referer || !whitelist.find((url) => new URL(referer).host.includes(url))) && url.pathname.includes('internal') ) { return new Response('', { status: 400 }) } ...中略... const imageRequest = new Request(`https://imagedelivery.net/${accountHash}/${pathname}/${queryParams}`, { headers: request.headers }) return fetch(imageRequest, options); } };
ファイル管理・アップロード
最後にファイルの管理とアップロードについては、ファイル管理用のリポジトリを作成し、GitHub Actions を通じてコンテンツは Cloud Storage に、画像は Cloudflare Images にアップロードすることで全体の管理を実現しました。最終的な全体構成は以下の通りです。
CDN導入後のパフォーマンス
CDN の導入前は、全体を通して150ms〜250ms
の取得時間がかかっていましたが、ブラウザキャッシュが適用された後は最速0ms
、エッジキャッシュでも50ms〜100ms
と、かなりの高速化を実現しました!
サービスを使用しても体感できるほどの表示速度が速くなっており、ユーザーの体験もかなり向上したかと思います。
おわりに
現在キカガクでは、急成長するプロダクトを支えるソフトウェアエンジニアを絶賛募集中です。キカガクに興味を持ってくださった方は採用ページをご確認いただき、ぜひ一度カジュアル面談でざっくばらんにお話しましょう!