Skip to content

オブジェクトストレージと CDN

💡 学習ガイド:この記事では、ファイルアップロードからユーザーダウンロードまでの完全なフローを解説します。オブジェクトストレージがどのように「スマート倉庫」のように大量のファイルを管理し、CDN がどのように「宅配便ネットワーク」のようにコンテンツをユーザーの目の前に届けるのか、そしてその途中にある「落とし穴」について学びます。基本的な HTTP リクエストと DNS 解決の仕組みを事前に理解しておくことをおすすめします。

始める前に、いくつかの「基礎知識」を補強しておくことをおすすめします:


0. はじめに:なぜファイルのアップロードとダウンロードはこんなに「遅い」のか?

こんなシーンを想像してみてください:あなたは写真コミュニティに 10MB の高解像度写真をアップロードしましたが、完了するまでに 30 秒もかかりました。一方、北京にいる友達はクリックしてわずか 2 秒でダウンロードできました。同じファイルなのに、なぜアップロードとダウンロードの体験はこれほど違うのでしょうか?

あるいはこう考えてみてください:あなたの EC サイトがダブルイレブン(独身の日)セールを開催し、商品詳細ページに突然数百万のトラフィックが殺到して、サーバーが「ダウン」してしまいました。帯域幅が足りなかったのか?それともアーキテクチャ設計に問題があったのか?

これらの疑問の答えは、すべてオブジェクトストレージCDN という「黄金のパートナー」コンビに隠されています。


1. オブジェクトストレージ:あなたの「スマートクラウド倉庫」

1.1 オブジェクトストレージとは?

従来のファイルシステムは、あなたの家のクローゼットのようなものです:衣類を「トップス/パンツ/スカート」と階層ごとに整理し、シャツを探すには、クローゼットを開ける→トップスエリア→シャツ棚、という手順を踏みます。この「階層ネスト」モデルは、ファイル数が爆発的に増加すると極めて扱いにくくなります。

一方、オブジェクトストレージは現代の倉庫物流のようなものです:各荷物には一意の「追跡番号」(オブジェクトキー)が振られており、番号を伝えるだけで、倉庫ロボットが膨大な荷物の中から正確に取り出してくれます。

🗄️Object Storage ArchitectureUnderstand the relationship between Bucket, Object, and Metadata.
📦BucketsNamespace isolation and permission control
🖼️
myapp-images-prod
12543 objects
256 GB
🎬
myapp-videos-prod
892 objects
1.2 TB
💾
myapp-backups
3456 objects
500 GB
📄ObjectsFile data + metadata
Click a bucket above to view objects.
💡Core idea:Object storage uses a three-level structure: Account → Bucket → Object. Each object carries rich metadata for retrieval and management.

主な違いの一覧

観点従来のファイルシステムオブジェクトストレージ
編成方式階層ディレクトリツリーフラットなキー・バリュー形式
アクセスプロトコルPOSIX(ローカルファイル操作)HTTP/REST API
拡張性単一マシンの容量に制限ありほぼ無限の水平拡張
メタデータ基本属性(サイズ、時刻)豊富なカスタムメタデータ
一般的なユースケースローカルのオフィス文書画像/動画/バックアップ/静的アセット

1.2 オブジェクトストレージのコア概念

バケット(Bucket):あなたの「倉庫区画」

バケットはオブジェクトストレージのトップレベルコンテナで、独立した名前空間に相当します。すべてのオブジェクトは必ずいずれかのバケットに格納されます。

命名規則(Alibaba Cloud OSS を例に):

  • グローバルで一意:クラウドプロバイダー全体の全ユーザー間で重複不可
  • 小文字アルファベット、数字、ハイフンのみ使用可能
  • 小文字アルファベットまたは数字で始まり、終わる必要あり
  • 長さは 3〜63 文字

実践での落とし穴:かつてあるチームが事業ラインごとに数十個のバケットを作成したところ、月末の請求書を見て愕然としました——各バケットに最低ストレージ料金とリクエスト料金がかかっていたのです。アドバイス:「環境+用途」の組み合わせでバケットを計画しましょう。例:prod-static-assetsdev-backup-archive

オブジェクト(Object):あなたの「データの荷物」

オブジェクトはストレージの基本単位で、3 つの部分から構成されます:

  1. キー(Key):オブジェクトの一意識別子、「追跡番号」に相当

    • 例:images/avatar/2024/user123.jpg
    • パスのように見えますが、本質的には単なる文字列です
  2. データ(Data):オブジェクトのコンテンツ本体

    • 任意のバイナリデータが可能
    • サイズ制限はクラウドプロバイダーによって異なります(通常、単一オブジェクト 5TB 以内)
  3. メタデータ(Metadata):オブジェクトを説明する付加情報

    • システムメタデータ:Content-Type、ETag、Last-Modified など
    • カスタムメタデータ:例 x-oss-meta-ownerx-oss-meta-project

アクセス制御:誰が私の「倉庫」を操作できるのか?

オブジェクトストレージは多層的な権限制御を提供します:

レベル制御方式一般的なユースケース
バケットレベルBucket Policy(リソースポリシー)すべての外部ネットワークアクセス禁止、特定 IP のみ許可
オブジェクトレベルACL(アクセス制御リスト)公開画像、プライベートドキュメント
一時認証STS(Security Token Service)フロントエンド直接アップロード、モバイルアップロード

安全のレッドライン:AccessKey ID と AccessKey Secret をフロントエンドのコードに絶対に書かないでください!正しいやり方は:フロントエンドがあなたのバックエンドに一時 STS 認証情報をリクエストし、バックエンドが ID を検証した後、有効期限付きの一時認証情報を返します。


2. CDN:あなたの「グローバル宅配ネットワーク」

2.1 なぜ CDN が必要なのか?

あなたがネットショップを開き、サーバーを深圳に置いていると想像してください。今、北京のユーザーがあなたの画像にアクセスしています:

  • CDN がない場合:リクエストは北京→河北→河南→湖北→湖南→広東→深圳と、2000 キロ以上を横断し、往復で 4000 キロ以上になります。ネットワーク転送だけでも数十ミリ秒かかり、ネットワーク混雑時にはさらに悪化します。

  • CDN がある場合:リクエストは北京から直接北京の CDN ノードへ(おそらく北京の China Unicom データセンター内)、距離は 2000 キロから 20 キロに、遅延は 50ms から 5ms になります。

これが CDN のコアバリューです:コンテンツをユーザーにより近づけること

🌐How CDN Acceleration WorksHow edge nodes, origin server, and origin fetch work together.
👥Global Users
👤
Beijing user
👤
Shanghai user
👤
Guangzhou user
👤
Chengdu user
👤
Overseas user
🌐CDN Edge Nodes
🌐
Beijing node
North China
Cache2.5 TB
Hit92%
🌐
Shanghai node
East China
Cache3.1 TB
Hit89%
🌐
Guangzhou node
South China
Cache1.8 TB
Hit87%
🌐
Chengdu node
Southwest China
Cache1.2 TB
Hit85%
🏢Origin Server
🗄️
Object storage origin
bucket.oss-cn-beijing.aliyuncs.com
Healthy
🎮 Simulation
📊 Access Stats
0
Cache hits
0
Cache misses
0%
Hit rate
0ms
Avg response
💡Core idea:CDN is like opening branches worldwide: users get resources from the nearest branch instead of always visiting the main store.

2.2 CDN のコアアーキテクチャ

エッジノード:ユーザーに最も近い「宅配ステーション」

エッジノードは CDN ネットワークにおいてユーザーに最も近い階層で、通常以下の場所に展開されます:

  • 通信事業者のデータセンター(China Unicom/China Telecom/China Mobile)
  • 大都市のインターネットエクスチェンジセンター
  • 主要な交通ハブ

中国の主要 CDN ノード分布

  • 一線都市:北京、上海、広州、深圳
  • 二線都市:杭州、南京、成都、武漢、西安
  • 海外:香港、シンガポール、東京、シリコンバレー、フランクフルト

Edge Node Distribution Demo

Shows global CDN edge-node distribution and scheduling strategy.

オリジンサーバー:コンテンツの「総合倉庫」

オリジンサーバーは CDN がコンテンツを取得するためにバックトゥオリジンする場所で、以下が利用可能です:

  • オブジェクトストレージ(OSS/COS/S3)
  • 自社サーバー(ECS/物理マシン)
  • ロードバランサー(SLB/CLB)

重要な設定

  • バックトゥオリジン HOST:CDN ノードがオリジンサーバーにアクセスする際に使用するドメイン/IP
  • バックトゥオリジンプロトコル:HTTP か HTTPS か
  • バックトゥオリジンポート:80、443、またはカスタムポート

中間層ノード:「地域中継センター」

エッジノードとオリジンサーバーの間には、通常 1 層以上の中間ノードがあります:

  • 集約ノード:複数のエッジノードからのバックトゥオリジンリクエストを集約し、オリジンサーバーの負荷を軽減
  • 地域センター:一大地域のコンテンツ配信とスケジューリングを担当

この階層アーキテクチャの利点:

  1. オリジンサーバー負荷の軽減:1000 のエッジノードからのリクエストが、オリジンサーバーへは 10 回だけになる可能性も
  2. ヒット率の向上:人気コンテンツは中間層でインターセプトされ、バックトゥオリジン不要に
  3. 障害の隔離:特定のリンクに問題が発生しても、自動的に他のパスに切り替え可能

2.3 CDN 高速化の完全な流れ

実際のユーザーリクエストを追跡してみましょう:

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

Step 1:DNS 解決(インテリジェントスケジューリング)

ユーザー入力:cdn.example.com/image.jpg

DNS サーバーが返す:北京 Unicom CDN ノード IP(1.2.3.4)

ここでの鍵はインテリジェント DNS:ユーザーの通信事業者、地理位置情報、ノード負荷に基づいて、最適な CDN ノード IP を返します。

Step 2:エッジノード検索(キャッシュヒット?)

リクエストが北京 Unicom CDN ノード(1.2.3.4)に到着

ノードがローカルキャッシュをチェック:
├─ ヒット?コンテンツを直接返す ✓
└─ ミス?次のステップへ

Step 3:バックトゥオリジン取得(階層を上がっていく)

エッジノードでミス

親ノード(例:華北地域センター)にリクエスト
├─ 親ノードでヒット?コンテンツを返す
└─ 親ノードもミス?さらに上へ

    オリジンサーバーにリクエスト

    オリジンサーバーがコンテンツを返す

Step 4:キャッシュして返す(次回はより速く)

コンテンツがリンクを逆方向に返る

各層のノードがそれぞれキャッシュを保持

最終的にユーザーに到着

こうすることで、次回誰かが同じファイルをリクエストしたときには、エッジノードから直接返せるようになり、「一瞬で開く」体験を実現します。


3. アップロードからアクセスまで:完全なフロー解析

3.1 ファイルアップロードの 3 つの方式

📤File Upload FlowUnderstand direct upload, multipart upload, and resumable upload.
🚀
Direct upload
Upload small files to object storage in one request
Best for: < 100MB
🔪
Multipart upload
Split large files into parts and upload in parallel
Best for: > 100MB
💾
Resumable upload
Continue from the breakpoint after network interruption
Best for: Any size
🚀 Direct Upload Flow
1
User selects file
Browser selects a 5MB image file
2
Request upload credential
Frontend → backend → temporary STS credential
3
Upload directly to object storage
Browser → OSS/COS, 5MB in one request
4
Upload complete
Return URL; frontend asks backend to save record
💡Core idea:Multipart upload improves reliability for large files. If the network breaks, resumable upload avoids sending the whole file again.

方式 1:クライアント → サーバー → オブジェクトストレージ(従来型)

ブラウザ → あなたのバックエンドサーバー → オブジェクトストレージ

フロー

  1. ユーザーがファイルを選択し、アップロードをクリック
  2. ファイルがまずあなたのバックエンドサーバーにアップロードされる
  3. バックエンドが完全なファイルを受信した後、オブジェクトストレージに転送アップロード
  4. アップロード結果をユーザーに返す

メリット

  • 実装がシンプルで、フロントエンド・バックエンドともに制御しやすい
  • バックエンドでファイル検証、フォーマット変換が可能
  • 機密操作のログ記録、権限チェックが可能

デメリット

  • 帯域幅の二重消費:ユーザーアップロードで 1 回、サーバー転送でさらにもう 1 回帯域を消費
  • サーバー負荷大:大きなファイルは多くのメモリと CPU を消費
  • アップロードが遅い:中継が 1 つ増えるため、ユーザーが体感するアップロード時間が長くなる

適用シーン:小さなファイル(<10MB)、バックエンド処理が必要な場合(画像圧縮、ウォーターマーク付与など)、内部管理システム。

方式 2:クライアントからオブジェクトストレージへ直接アップロード(モダン推奨)

ブラウザ ──────→ オブジェクトストレージ

        バックエンドは一時認証情報のみ発行

フロー

  1. ユーザーがファイルを選択、フロントエンドがまずバックエンドに「アップロード認証情報」をリクエスト
  2. バックエンドがユーザー ID を検証し、オブジェクトストレージサービスに一時 STS 認証情報(有効期限付き)を申請
  3. バックエンドが一時認証情報をフロントエンドに返す
  4. フロントエンドが認証情報を持って、直接オブジェクトストレージにファイルをアップロード
  5. オブジェクトストレージがアップロード結果を返し、フロントエンドがバックエンドに「アップロード完了」を通知

メリット

  • アップロードが速い:中継ステップが減り、ユーザー体感速度が最速
  • サーバー負荷が小さい:認証情報発行のみを処理し、ファイルストリームを処理しない
  • 帯域幅の節約:アップロードトラフィックが 1 回で済む
  • セキュリティが高い:一時認証情報には有効期限があり、漏洩しても被害は限定的

デメリット

  • 実装がやや複雑で、STS、署名メカニズムの理解が必要
  • フロントエンドでマルチパートアップロード、中断再開などのロジックを処理する必要あり
  • クロスオリジン(CORS)の設定が必要

適用シーン:大きなファイルのアップロード、ユーザー生成コンテンツ(UGC)、高同時実行アップロードが必要なビジネス。

方式 3:マルチパートアップロード+中断再開(大容量ファイル必須)

10GB 動画ファイル

1000 個の 10MB パートに分割

並列アップロード(同時に 5 パート)

ネットワーク断!600 パートまでアップロード済み

ネットワーク復旧、601 パート目から再開

全パート完了、「結合」リクエストを発行

なぜマルチパートが必要なのか?

シーン非マルチパートマルチパート
ネットワーク変動99% 転送後に切断、全再送失敗したパートのみ再送
アップロード速度シングルスレッド、遅いマルチスレッド並列、速い
メモリ使用量ファイル全体をキャッシュ必要現在のパートのみキャッシュ
進捗表示0% と 100% のみ各パートの進捗を正確に表示

主要クラウドプロバイダーのマルチパート仕様

プロバイダーパートサイズ制限最大パート数最小パートサイズ
Alibaba Cloud OSS100MB10000100KB
Tencent Cloud COS5GB100001MB
AWS S35GB100005MB(推奨)
Qiniu Cloud100MB100004MB

3.2 CDN バックトゥオリジン戦略の詳細

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

「バックトゥオリジン」とは?

CDN エッジノードはオリジンサーバーのコンテンツをキャッシュしていますが、以下の場合:

  • ユーザーがリクエストしたコンテンツが初めてアクセスされる
  • キャッシュされたコンテンツが期限切れ(TTL 満了)
  • キャッシュが手動でパージ/プリフェッチされた

CDN ノードはオリジンサーバーに最新コンテンツをリクエストする必要があり、このプロセスを「バックトゥオリジン」と呼びます。

バックトゥオリジンの 3 つのモード

モード原理適用シーンメリット・デメリット
直接バックトゥオリジンCDN ノード → オリジンサーバーオリジンにパブリック IP があり、トラフィックが少ない場合シンプルで直接的だが、オリジン負荷大
中間オリジン経由CDN ノード → 中間層 → オリジン大規模サイト、多層キャッシュアーキテクチャオリジン負荷を分散、アーキテクチャ複雑
OSS/COS をオリジンにCDN ノード → オブジェクトストレージ静的アセット、画像、動画ベストプラクティス、低コスト・高性能

バックトゥオリジン設定の実践

シーン 1:オブジェクトストレージをオリジンにする(推奨)

ユーザーアクセス:cdn.example.com/images/photo.jpg

            CDN エッジノード(北京)

            ミス、オリジンにバックトゥオリジン

            オリジン:bucket-name.oss-cn-beijing.aliyuncs.com

            画像を返し、CDN がキャッシュしてユーザーに応答

主要な設定項目:

  • オリジンタイプ:OSS/COS ドメイン または カスタムオリジン
  • バックトゥオリジンプロトコル:HTTP か HTTPS か(HTTPS 推奨)
  • バックトゥオリジン HOST:オリジンサーバーアクセス時に使用する Host ヘッダー
  • バックトゥオリジン SNI:HTTPS バックトゥオリジン時のサーバー名表示

シーン 2:複数オリジンのロードバランシング

単一のオリジンサーバーがバックトゥオリジンの負荷に耐えられない場合、複数のオリジンを設定できます:

CDN エッジノード
    ├─ オリジン A (重み 50%)
    ├─ オリジン B (重み 30%)
    └─ オリジン C (重み 20%)

アクティブ/スタンバイモード:

CDN エッジノード
    ├─ プライマリオリジン A (正常時は全トラフィック)
    └─ バックアップオリジン B (プライマリ障害時に切り替え)

バックトゥオリジン帯域幅 vs CDN 帯域幅

ここで混同しやすい概念があります:

指標定義課金関係
CDN ダウンリンク帯域幅CDN ノードからユーザーへのトラフィック通常はトラフィック課金の CDN 費用
バックトゥオリジン帯域幅オリジンから CDN ノードへのトラフィック通常はオブジェクトストレージまたはオリジンのアウトバウンドトラフィック費用

コスト削減のコツ

  • CDN ヒット率を向上させる(より多くのリクエストをキャッシュで処理し、バックトゥオリジンを削減)
  • 適切なキャッシュ時間(TTL)を設定
  • プリフェッチ機能を使い、ユーザーアクセス前にホットコンテンツをキャッシュ
  • 「301/302 フォロー」を有効にして、不要なバックトゥオリジンリダイレクトを回避

3.3 キャッシュ戦略の設定

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

キャッシュキー(Cache Key):何が「同じファイル」かを決定する

CDN は、2 つのリクエストが同じキャッシュコピーを返すべきかをどのように判断するのでしょうか?その鍵となるのがキャッシュキーです。

デフォルトのキャッシュキーには通常以下が含まれます

  • URL パス(クエリパラメータを含まない)
  • 例:/images/photo.jpg

問題となるシーン

ユーザー A のリクエスト:/images/photo.jpg?w=100&h=100  (100x100 サムネイル)
ユーザー B のリクエスト:/images/photo.jpg?w=800&h=600  (800x600 大画像)

キャッシュキーにパスのみが含まれている場合、異なるサイズの 2 つの画像が同じファイルとみなされ、混乱が生じます。

解決策:カスタムキャッシュキールール

ルール効果
指定クエリパラメータを保持wh を保持異なるサイズを別々にキャッシュ
全クエリパラメータを保持すべて保持完全な正確一致
特定のクエリパラメータを無視tokentimestamp を無視タイムスタンプ付き URL でもキャッシュヒット可能
リクエストヘッダーを含めるAccept-Language を含める異なる言語で異なるコンテンツを返す

実践設定例(Alibaba Cloud CDN):

キャッシュキールール:
- URL パス:/images/*
- 保持するクエリパラメータ:w, h, format
- 無視するクエリパラメータ:token, timestamp, utm_source

キャッシュ時間(TTL):コンテンツの「鮮度」のバランス

TTL(Time To Live)は、コンテンツが CDN ノードにキャッシュされる期間を決定します。短すぎるとバックトゥオリジンが多くコスト高に、長すぎるとコンテンツ更新後にユーザーが古いコンテンツを見ることになります。

ファイルタイプ別 TTL 設定の推奨

ファイルタイプ推奨 TTL理由
HTML ページ0〜5 分コンテンツが頻繁に更新され、リアルタイム性が必要
JS/CSS ファイル1 年(ファイル名 hash と併用)コンテンツ不変、ファイル名変更でキャッシュ無効化
画像/動画7〜30 日更新頻度が低く、長期キャッシュ可能
フォントファイル1 年ほぼ不変
API レスポンス0〜5 分(ビジネスによる)データのリアルタイム性要求が高い

フロントエンドエンジニアリングと CDN のベストプラクティス

javascript
// webpack/vite 設定
output: {
  filename: 'js/[name]-[contenthash:8].js',
  chunkFilename: 'js/[name]-[contenthash:8].chunk.js',
}

生成されるファイル名:app-a3f2b1c9.js

  • ファイル内容の変更 → hash 変更 → 新しい URL → 自然にキャッシュ無効化
  • ファイル内容不変 → hash 不変 → URL 不変 → 長期キャッシュヒット

キャッシュのパージとプリフェッチ

手動パージ(緊急時)

オリジンサーバーのコンテンツを更新したが、CDN キャッシュがまだ期限切れでない場合、ユーザーにはまだ古いコンテンツが見えています:

パージタイプ効果所要時間適用シーン
URL パージ指定 URL のキャッシュを無効化5〜10 分単一ファイル更新
ディレクトリパージ指定ディレクトリ以下の全コンテンツを無効化10〜30 分一括更新
サイト全体パージドメイン全体のキャッシュをすべて無効化30 分以上緊急ロールバック

重要注意事項:パージはキャッシュを無効にするだけで、次のリクエストでバックトゥオリジンして新しいコンテンツを取得します。ピーク時に大量パージを行うと、オリジンサーバーが過負荷になる可能性があります。

プリフェッチ(プロアクティブ最適化)

パージは受動的(コンテンツが既に更新された後)ですが、プリフェッチは能動的(事前にキャッシュ)です。

シーン:明日の午前 10 時に話題の記事を公開予定

今夜のうちにプリフェッチリクエストを送信:
- URL: https://cdn.example.com/articles/話題の記事.html
- プリフェッチ範囲:全国の全エッジノード

効果:
明日の 10 時にユーザーがアクセスした時点で、コンテンツはすでにエッジノードで待機済み
→ バックトゥオリジン遅延ゼロ、一瞬で開く体験

4. トラフィックスケジューリング:ユーザーを「最も近い」ノードへアクセスさせる

🚦Traffic SchedulingUnderstand CDN intelligent scheduling and load balancing.
💡Core idea:Intelligent scheduling combines nearest access, load balancing, and failover to provide global acceleration and high availability.

4.1 インテリジェント DNS スケジューリング

従来の DNS 解決:

ユーザーの問い合わせ:cdn.example.com の IP は?
DNS の回答:1.2.3.4(固定)

インテリジェント DNS 解決:

ユーザー(北京 Unicom)の問い合わせ:cdn.example.com の IP は?
インテリジェント DNS:確認します... 北京 Unicom の CDN ノードは 1.2.3.4

ユーザー(上海 Telecom)の問い合わせ:cdn.example.com の IP は?
インテリジェント DNS:上海 Telecom の CDN ノードは 5.6.7.8

スケジューリングの次元

次元説明効果
地理位置情報省/市/国別に割り当て近距離アクセス、遅延低減
通信事業者Unicom/Telecom/Mobile/BGP同一事業者内転送、クロスネットワーク回避
ノード負荷リアルタイム CPU/帯域幅/QPS過負荷ノードを回避
ノード健全性可用性プローブ障害ノードを自動除外
コスト要因帯域幅単価の差異パフォーマンスとコストのバランス

4.2 HTTP DNS と IP 直接接続

従来の DNS には問題があります:DNS ハイジャックと解決遅延

HTTP DNS ソリューション

クライアント → システム DNS をバイパス → HTTP DNS サービスに直接問い合わせ(例:223.5.5.5:80)

    最適な IP リストを返す(重み付き)

    クライアントがネットワーク品質プローブに基づいて最適な IP を選択

利点:

  • ハイジャック防止:通信事業者の DNS を経由しない
  • より正確:クライアントのネットワーク品質に基づいて IP を選択可能
  • リアルタイム性:障害切り替えがより速い

実践アドバイス

  • モバイルアプリでは HTTP DNS の導入を強く推奨
  • Web 側では CDN 提供の CNAME スケジューリングを使用可能
  • クリティカルなビジネスではマルチ IP 災害復旧(1 つのドメインが複数 IP を返す)を実施可能

5. HTTPS 最適化:セキュリティとパフォーマンスのバランス

🔒HTTPS OptimizationUnderstand CDN HTTPS protocol and certificate management.
💡Core idea:HTTPS encrypts traffic with TLS/SSL to prevent man-in-the-middle attacks and data leakage. It is a security baseline for modern web apps.

5.1 なぜ CDN で HTTPS が重要なのか?

シーン比較

HTTPS なし:
ユーザーが http://cdn.example.com/image.jpg にアクセス

ブラウザのアドレスバーに「安全ではありません」と表示

一部のブラウザ/アプリが直接アクセスをブロック

SEO ランキング低下
HTTPS あり:
ユーザーが https://cdn.example.com/image.jpg にアクセス

ブラウザに緑色の鍵アイコンを表示

HTTP/2 多重化が有効に

パフォーマンス + セキュリティのダブル向上

5.2 CDN HTTPS 設定のポイント

証明書管理

方式説明コスト適用シーン
クラウドプロバイダー無料証明書Alibaba Cloud/Tencent Cloud 等が提供無料単一ドメイン、クイックスタート
Let's Encryptコミュニティ無料証明書無料自動化デプロイ
商用 DV/OV/EV 証明書Symantec、GeoTrust 等年間数百〜数万元エンタープライズ、グリーンバー必要時
ワイルドカード証明書*.example.com年間数千元複数サブドメイン

実践アドバイス

  • テスト環境:Let's Encrypt またはクラウドプロバイダー無料証明書
  • 本番環境:ワイルドカード証明書(手間削減)または単一ドメイン OV 証明書(コスト削減)
  • 証明書の有効期限に注意し、自動更新リマインダーを設定

HTTPS 最適化設定

TLS バージョン選択

推奨設定:TLS 1.2 と TLS 1.3 のみ
互換設定:TLS 1.1 + TLS 1.2 + TLS 1.3(古いブラウザ互換用)

暗号スイート

推奨:ECDHE 鍵交換 + AES-GCM 暗号化
無効化:DES、RC4、MD5、SHA1

OCSP Stapling

機能:CDN ノードが証明書失効ステータスを事前取得
効果:クライアント検証時間を 200-500ms 短縮
アドバイス:必ず有効に

TLS セッション再利用

Session ID 再利用:クライアントが前回の Session ID を持参、サーバーがセッションを復元
Session Ticket 再利用:サーバーがセッション状態を暗号化してクライアントに送信、次回持参
効果:完全な TLS ハンドシェイクを回避、1-RTT 削減

5.3 CDN での HTTP/2 と HTTP/3 の応用

HTTP/2 多重化

HTTP/1.1:
リクエスト 1 (index.html) ────────────────→
レスポンス 1 ←──────────────────────────────
リクエスト 2 (style.css) ─────────────────→
レスポンス 2 ←──────────────────────────────
リクエスト 3 (script.js) ─────────────────→
レスポンス 3 ←──────────────────────────────
(直列、1 つ完了してから次へ)

HTTP/2:
リクエスト 1 ──→
リクエスト 2 ──→   1 つの TCP 接続に統合、フレームインターリーブ転送
リクエスト 3 ──→
レスポンス 1 ←──   優先度に基づいてストリーミング返却
レスポンス 2 ←──
レスポンス 3 ←──
(並列、1 接続で多重化)

HTTP/2 サーバープッシュ

シーン:ユーザーが index.html をリクエスト、その中で style.css と script.js を参照

従来方式:
1. ユーザーが index.html をダウンロード
2. 解析して style.css と script.js が必要と判明
3. さらに 2 つのリクエストを送信して取得

HTTP/2 プッシュ:
1. ユーザーが index.html をリクエスト
2. CDN ノードが index.html を返すと同時に、style.css と script.js を能動的にプッシュ
3. ユーザーが html を解析する時点で、リソースはすでにキャッシュ内

注意:プッシュは慎重に、多すぎると帯域を浪費、少なすぎると効果なし

HTTP/3 (QUIC)

HTTP/2 の問題:TCP ベース、ヘッドオブラインブロッキング
→ 1 つの TCP パケットが失われると、接続全体が再送を待機

HTTP/3 の解決策:QUIC(UDP 上に信頼性のある転送を実装)
→ 各ストリームが独立、1 つのストリームのブロックが他のストリームに影響しない
→ 接続移行:WiFi → 4G 切り替えでも接続が途切れない
→ 0-RTT ハンドシェイク:初回アクセスでも高速に接続確立

現状:2024 年、主要 CDN は HTTP/3 対応済み、有効化を推奨

6. アクセス分析:CDN レポートを読み解く

📊Access AnalyticsUnderstand CDN access statistics and log analytics.
💡Core idea:Log analytics shows who accessed which resources and when, helping detect unusual access patterns and security events.

6.1 コア指標の解説

帯域幅(Bandwidth)

定義:単位時間あたりの転送データ量
単位:bps(ビット毎秒)、Mbps、Gbps

CDN 帯域幅 = 全エッジノードのアウトバウンドトラフィック合計

以下の区別に注意:
- 課金帯域幅:通常 95 パーセンタイルピークまたは日次ピークで課金
- 実帯域幅:リアルタイム転送レート

帯域幅とトラフィックの関係

1 Mbps の帯域幅で 1 時間継続実行 = 450 MB のトラフィック
(計算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB)

QPS(Queries Per Second)

定義:毎秒のクエリ/リクエスト数

CDN QPS = 全エッジノードが毎秒処理する HTTP リクエスト総数

注意:QPS が高くても帯域幅が高いとは限らない
- 小さなファイルのシーン:QPS は高いが、帯域幅は高くない
- 大きなファイルのシーン:QPS は高くないが、帯域幅は高い

ヒット率(Hit Ratio)

定義:CDN エッジノードでヒットしたリクエストの総リクエストに対する割合

計算式:
ヒット率 = (ヒット数 / 総リクエスト数) × 100%
または
ヒット率 = (1 - バックトゥオリジントラフィック / 総アウトバウンドトラフィック) × 100%

業界標準:
- 画像/動画/JS/CSS:> 95%
- HTML ページ:50〜80%(更新頻度による)
- API インターフェース:通常キャッシュしないか、ごく低い

ヒット率が低い一般的な原因

原因現象解決策
キャッシュ時間が短すぎるTTL が数分しかないファイルタイプに応じて TTL を調整
クエリパラメータ変動URL にランダム数値が付与特定パラメータを無視するよう設定
キャッシュキー設定不適切区別すべきでないものが区別されているキャッシュキールールを最適化
コンテンツ更新頻繁ファイルが頻繁に上書きされるバージョン番号または hash ファイル名を使用
初回アクセスが多い新しいコンテンツまたは新しいノード事前にプリフェッチ

6.2 ログ分析と問題調査

CDN ログフィールド解析

典型的な CDN アクセスログには以下のフィールドが含まれます:

時刻 | クライアント IP | リクエストメソッド | URL | HTTP ステータスコード | レスポンスサイズ | キャッシュステータス | レスポンスタイム | Referer | User-Agent

例:
2024-01-15 14:32:01 | 114.114.114.114 | GET | https://cdn.example.com/images/photo.jpg | 200 | 153600 | HIT | 23 | https://example.com/ | Mozilla/5.0...

主要フィールドの説明:

フィールド説明分析価値
cache_statusキャッシュステータスHIT(ヒット)、MISS(ミス)、EXPIRED(期限切れ)
response_timeレスポンスタイム(ms)ユーザー体験の判断、>500ms は最適化が必要
http_statusHTTP ステータスコード404/500 エラー調査
bytes_sent送信バイト数帯域幅統計

よくある問題の調査

問題 1:ユーザーからアクセスが遅いと報告

調査手順:

1. ログの response_time を確認
   - 大きい場合(>500ms):キャッシュ MISS かオリジンが遅いかをチェック

2. cache_status をチェック
   - HIT:キャッシュヒット、遅延はファイルが大きいかノード問題の可能性
   - MISS:ミス、キャッシュ戦略またはヒット率の最適化が必要

3. クライアント IP 分布をチェック
   - 特定地域が遅い:そのノードの負荷が高いかカバレッジ不足の可能性

問題 2:キャッシュが効かず、毎回バックトゥオリジンしている

チェックリスト:

□ オリジンレスポンスヘッダーに Cache-Control: no-cache / private がないか?
□ URL にランダムパラメータ(例:?_=123456)が付いていないか?
□ キャッシュキー設定は正しいか?
□ TTL 設定が短すぎないか?
□ CDN ではなくブラウザのローカルキャッシュにヒットしていないか?

問題 3:費用が急増

調査の方向性:

1. 請求明細を確認
   - CDN トラフィック費が高い:大容量ファイルが頻繁にアクセスされているか、または不正リンクの可能性
   - バックトゥオリジントラフィック費が高い:ヒット率が急落していないかチェック
   - リクエスト数費用が高い:CC 攻撃やクローラーがないかチェック

2. アクセスログを確認
   - 大量の 404 リクエストがないか(スキャンまたは設定ミスの可能性)
   - Referer が異常でないか(不正リンクの判断)

3. セキュリティ設定
   - 不正リンク防止を有効化(Referer ホワイトリスト)
   - IP ブラックリスト/ホワイトリストを有効化
   - CC プロテクションを設定

7. 実践ケーススタディ:ゼロから画像高速化ソリューションを構築

7.1 ビジネスシーン

あなたが写真コミュニティの技術責任者で、以下の課題に直面しているとします:

  • ユーザーアップロード:毎日 100 万枚の画像をアップロード(平均 2MB/枚)
  • ユーザーアクセス:毎日 5000 万回の画像表示リクエスト
  • アクセス分布:ユーザーは全国に分布、海外からも少数のアクセスあり
  • パフォーマンス要件:画像読み込み時間 < 500ms
  • コスト予算:月額 5 万元以内に抑えたい

7.2 アーキテクチャ設計

                         ┌──────────────────────────────────────┐
                         │          ユーザーアップロードフロー        │
                         └──────────────────────────────────────┘

   ユーザーブラウザ                              バックエンドサービス                  オブジェクトストレージ
       │                                            │                            │
       │  1. アップロード認証情報を申請              │                            │
       │───────────────────────────────────────────>│                            │
       │                                            │                            │
       │                                            │  2. STS 一時認証情報を申請    │
       │                                            │───────────────────────────>│
       │                                            │                            │
       │                                            │  3. STS 認証情報を返す       │
       │                                            │<───────────────────────────│
       │                                            │                            │
       │  4. アップロード認証情報を返す(STS 含む)  │
       │<───────────────────────────────────────────│                            │
       │                                            │                            │
       │  5. ファイルを直接アップロード(STS 署名使用)│
       │──────────────────────────────────────────────────────────────────────>│
       │                                            │                            │
       │  6. アップロード結果を返す(URL、ETag 等)  │
       │<──────────────────────────────────────────────────────────────────────│
       │                                            │                            │
       │  7. アップロード完了をバックエンドに通知(DB 保存)│
       │───────────────────────────────────────────>│                            │


                         ┌──────────────────────────────────────┐
                         │           ユーザーアクセスフロー          │
                         └──────────────────────────────────────┘

   ユーザーブラウザ          DNS 解決              CDN ノード            オブジェクトストレージ(オリジン)
       │                     │                     │                     │
       │  1. 画像 URL をリクエスト                 │                     │
       │────────────────────────────────────────>│                     │
       │                     │                     │                     │
       │                     │  2. DNS クエリ      │                     │
       │                     │────────────────────>│                     │
       │                     │                     │                     │
       │                     │  3. 最適ノード IP を返す│                 │
       │                     │<────────────────────│                     │
       │                     │                     │                     │
       │  4. CDN ノードに接続│                     │                     │
       │────────────────────────────────────────>│                     │
       │                     │                     │                     │
       │                     │  5. キャッシュチェック│                  │
       │                     │                     ├─ ヒット?直接返す   │
       │                     │                     └─ ミス?続行         │
       │                     │                     │                     │
       │                     │                     │  6. バックトゥオリジン取得│
       │                     │                     │──────────────────>│
       │                     │                     │                     │
       │                     │                     │  7. ファイルを返す │
       │                     │                     │<──────────────────│
       │                     │                     │                     │
       │                     │  8. キャッシュして応答│                   │
       │<────────────────────────────────────────│                     │

7.3 主要設定の詳細

オブジェクトストレージ設定

ストレージバケット計画

 Bucket: myapp-images-prod
 ├─ ディレクトリ構造:
 │   ├─ uploads/           # ユーザーアップロードの元画像
 │   │   ├─ 2024/01/15/user123-abc.jpg
 │   │   └─ 2024/01/15/user456-def.png
 │   ├─ thumbnails/        # サムネイル
 │   │   ├─ small/         # 100x100
 │   │   ├─ medium/        # 400x300
 │   │   └─ large/         # 800x600
 │   └─ processed/         # 処理済み画像(ウォーターマーク等)

 ├─ アクセス権限:
 │   ├─ 元画像ディレクトリ:プライベート(署名付きアクセス必須)
 │   ├─ サムネイルディレクトリ:パブリック読み取り
 │   └─ クロスオリジン CORS:*.myapp.com のアクセスを許可

 └─ ライフサイクルポリシー:
     ├─ アップロード 7 日後:低頻度ストレージ(40% コスト削減)
     ├─ アップロード 90 日後:アーカイブストレージ(70% コスト削減)
     └─ アップロード 3 年後:自動削除(またはより安価なコールドストレージに移行)

CORS クロスオリジン設定

xml
<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>https://myapp.com</AllowedOrigin>
    <AllowedOrigin>https://www.myapp.com</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <ExposeHeader>x-oss-request-id</ExposeHeader>
    <MaxAgeSeconds>3600</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>

CDN 高速化設定

キャッシュ戦略設定

グローバルデフォルトルール:
├─ キャッシュキー:URL パス + w、h、format クエリパラメータを保持
├─ デフォルト TTL:7 日
└─ バックトゥオリジン HOST:自動追従

ファイルタイプ別詳細設定:
├─ *.html:
│   ├─ TTL:5 分
│   └─ メモリキャッシュからの読み取りを優先

├─ *.js, *.css:
│   ├─ TTL:1 年
│   └─ クエリパラメータを無視(ファイル名に hash があるため)

├─ *.jpg, *.png, *.gif, *.webp:
│   ├─ TTL:30 日
│   ├─ クエリパラメータを保持(w、h、format は動的クロップ用)
│   └─ 画像自動圧縮最適化を有効化

└─ /api/*:
    ├─ TTL:0(キャッシュしない)
    └─ 直接バックトゥオリジン

HTTPS 最適化設定

証明書設定:
├─ 証明書タイプ:ワイルドカード証明書 *.myapp.com
├─ デプロイ方式:CDN 管理画面でアップロード、自動更新
└─ 予備証明書:EV 証明書をメインドメイン用に(緑色アドレスバー表示)

TLS 設定:
├─ 最低 TLS バージョン:1.2(互換性とセキュリティのバランス)
├─ 最高 TLS バージョン:1.3
├─ 暗号スイート:強力な暗号スイートのみ有効化
├─ OCSP Stapling:有効
├─ TLS セッション再利用:Session Ticket 有効
└─ HSTS:有効(max-age=31536000)

HTTP/2 と HTTP/3:
├─ HTTP/2:有効(多重化、ヘッダー圧縮)
├─ HTTP/2 Server Push:必要に応じて有効化(Preload での代替を推奨)
└─ HTTP/3 (QUIC):有効(試験的機能、段階的にトラフィック拡大)

7.4 コスト管理戦略

費用構成分析

月次 CDN + オブジェクトストレージ費用構成:

CDN 部分:
├─ ダウンリンクトラフィック費(大部分、約 60%)
│   ├─ 中国本土:0.15-0.30 元/GB
│   ├─ アジア太平洋地域:0.40-0.80 元/GB
│   └─ 欧米:0.30-0.60 元/GB

├─ リクエスト数費用(小部分、約 5%)
│   ├─ HTTP:0.01-0.05 元/万回
│   └─ HTTPS:0.05-0.15 元/万回(TLS ハンドシェイクがリソースを消費するため)

├─ 帯域幅ピーク費用(オプションの課金方式)
│   └─ 95 パーセンタイルピーク課金:トラフィック変動が大きいシーンに適する

└─ 付加価値機能費(約 5%)
    ├─ HTTPS 証明書管理
    ├─ WAF プロテクション
    ├─ リアルタイムログプッシュ
    └─ エッジスクリプト/関数

オブジェクトストレージ部分:
├─ ストレージ容量費(約 15%)
│   ├─ 標準ストレージ:0.12-0.15 元/GB/月
│   ├─ 低頻度ストレージ:0.08-0.10 元/GB/月
│   └─ アーカイブストレージ:0.03-0.05 元/GB/月

├─ リクエスト費用(約 5%)
│   ├─ PUT:0.01-0.05 元/万回
│   └─ GET:0.005-0.01 元/万回

├─ データ取り出し費用(低頻度/アーカイブ)
│   └─ 早期削除または取り出しに追加費用

└─ バックトゥオリジンアウトバウンドトラフィック費(約 10%)
    └─ CDN からオブジェクトストレージへのバックトゥオリジントラフィック費

コスト削減テクニック実践

テクニック 1:ストレージ階層化、自動ライフサイクル管理

yaml
# ライフサイクルルール例
rules:
  - id: image-lifecycle
    prefix: uploads/
    transitions:
      # 7 日後に低頻度ストレージに移行、30% コスト削減
      - days: 7
        storageClass: IA
      # 90 日後にアーカイブストレージに移行、70% コスト削減
      - days: 90
        storageClass: Archive
    # 3 年後に自動削除
    expiration:
      days: 1095

テクニック 2:CDN ヒット率向上、バックトゥオリジン削減

ヒット率が 90% から 95% に向上すると何を意味するか?

仮定:
- 日次トラフィック:10 TB
- ヒット率 90%:バックトゥオリジン 1 TB
- ヒット率 95%:バックトゥオリジン 0.5 TB

節約されるバックトゥオリジントラフィック:0.5 TB/日 × 0.15 元/GB × 30 日 = 2250 元/月

テクニック 3:圧縮とフォーマット最適化

画像最適化ソリューション:
├─ 元画像をオブジェクトストレージに保存(直接外部公開しない)
├─ CDN で画像処理機能を有効化:
│   ├─ フォーマット自動変換:JPEG → WebP/AVIF(30-50% 削減)
│   ├─ 品質自動圧縮:視覚的可逆圧縮(20-40% 削減)
│   ├─ サイズ適応:デバイスに応じて適切なサイズを返す
│   └─ プログレッシブローディング:最初はぼやけて徐々に鮮明に
└─ 効果:帯域幅コスト 50-70% 削減

テクニック 4:帯域幅ピークキャップとアラート

yaml
# 帯域幅キャップ設定
bandwidth_cap:
  daily_limit: 500 # Mbps、日次ピーク超過時に CDN を自動停止
  monthly_limit: 10000 # GB、月間トラフィック超過時に停止

  # アラート閾値
  alerts:
    - threshold: 70% # 70% 到達でアラート
      channels: [sms, email]
    - threshold: 90% # 90% 到達で電話
      channels: [phone]

8. まとめ:オブジェクトストレージ + CDN の黄金律

8.1 アーキテクチャ設計原則

原則 1:動的と静的の分離

動的コンテンツ(API、HTML)→ オリジンサーバーまたはエッジ関数を使用
静的コンテンツ(画像、JS、CSS、動画)→ CDN + オブジェクトストレージを使用

原則 2:近距離サービス

ユーザーがいる場所にコンテンツをキャッシュする
→ カバレッジの広い CDN プロバイダーを選択
→ インテリジェント DNS スケジューリングを有効化
→ 重要なコンテンツは事前にプリフェッチ

原則 3:階層型キャッシュ

ブラウザローカルキャッシュ(最強)

CDN エッジノードキャッシュ(次強)

CDN 中間層/地域ノード(補完)

オブジェクトストレージ/オリジンサーバー(最後の砦)

原則 4:コストと体験のバランス

ストレージ階層化:ホットデータは標準ストレージ、コールドデータはアーカイブストレージ
キャッシュ戦略:高頻度コンテンツは長めの TTL、低頻度コンテンツは短めの TTL
圧縮最適化:WebP/AVIF フォーマット、インテリジェント品質圧縮
監視アラート:帯域幅キャップを設定し、異常トラフィックを防止

8.2 落とし穴回避チェックリスト

ストレージバケット命名と権限

  • [ ] バケット名はグローバルで一意、占有されないように
  • [ ] プライベートファイルをパブリック読み取りに設定しない
  • [ ] AccessKey をフロントエンドコードに書かない、STS 一時認証情報を使用
  • [ ] サーバーサイド暗号化(SSE)を有効化して機密データを保護

CDN キャッシュ設定

  • [ ] HTML ファイルの TTL は長すぎないように(5 分未満を推奨)
  • [ ] JS/CSS は hash 付きファイル名を使用し、TTL を 1 年に設定
  • [ ] キャッシュキーは適切に、ユーザー情報などの変数を含めない
  • [ ] 重要な更新後はキャッシュのパージまたはプリフェッチを忘れずに

HTTPS セキュリティ

  • [ ] 証明書の期限切れに注意、自動更新を設定
  • [ ] 最低 TLS バージョンは 1.2 を推奨
  • [ ] HSTS を有効化してダウングレード攻撃を防止
  • [ ] 機密 Cookie に Secure と HttpOnly を設定

コスト管理

  • [ ] 帯域幅キャップアラートを有効化し、異常トラフィックを防止
  • [ ] 低頻度/アーカイブストレージには最低保存期間と早期削除費用あり、ルールに注意
  • [ ] バックトゥオリジントラフィック費も高額、CDN ヒット率向上に努める
  • [ ] 定期的にアクセスログを分析し、ゾンビリソースをクリーンアップ

9. 実践コードテンプレート

9.1 フロントエンド直接オブジェクトストレージアップロード(JavaScript)

javascript
/**
 * オブジェクトストレージ直接アップロードユーティリティクラス
 * 対応:Alibaba Cloud OSS、Tencent Cloud COS、AWS S3
 */
class DirectUploader {
  constructor(config) {
    this.provider = config.provider // 'oss' | 'cos' | 's3'
    this.region = config.region
    this.bucket = config.bucket
    this.getCredentials = config.getCredentials // 一時認証情報を取得する関数
  }

  /**
   * STS 一時認証情報を取得
   */
  async fetchCredentials() {
    // バックエンドに一時認証情報を申請
    const credentials = await this.getCredentials()
    return {
      accessKeyId: credentials.accessKeyId,
      accessKeySecret: credentials.accessKeySecret,
      sessionToken: credentials.securityToken || credentials.sessionToken,
      expiration: credentials.expiration
    }
  }

  /**
   * アップロード署名を生成(フロントエンドで署名を計算する場合)
   */
  generateSignature(credentials, fileKey, fileType, options = {}) {
    const timestamp = new Date().toISOString()
    const date = timestamp.slice(0, 10).replace(/-/g, '')

    // プロバイダーによって署名アルゴリズムが若干異なる
    switch (this.provider) {
      case 'oss':
        return this._ossSignature(credentials, fileKey, date, options)
      case 'cos':
        return this._cosSignature(credentials, fileKey, date, options)
      case 's3':
        return this._s3Signature(credentials, fileKey, date, options)
      default:
        throw new Error('Unknown provider')
    }
  }

  /**
   * 単一ファイルアップロード(小さなファイル < 100MB)
   */
  async upload(file, options = {}) {
    const credentials = await this.fetchCredentials()
    const fileKey = this._generateFileKey(file, options.directory)

    const formData = new FormData()

    // フォームフィールドを構築(プロバイダーによってフィールド名が異なる)
    const formFields = this._buildFormFields(
      credentials,
      fileKey,
      file.type,
      options
    )
    Object.entries(formFields).forEach(([key, value]) => {
      formData.append(key, value)
    })

    formData.append('file', file)

    // アップロードリクエストを送信
    const uploadUrl = this._getUploadUrl()
    const response = await fetch(uploadUrl, {
      method: 'POST',
      body: formData,
      // 大容量ファイルのアップロード時はより長いタイムアウト設定が必要な場合あり
      signal: options.signal // AbortController によるアップロードキャンセル対応
    })

    if (!response.ok) {
      const errorText = await response.text()
      throw new Error(`Upload failed: ${response.status} ${errorText}`)
    }

    return {
      url: this._getFileUrl(fileKey),
      key: fileKey,
      etag: response.headers.get('ETag'),
      size: file.size
    }
  }

  /**
   * マルチパートアップロード(大容量ファイル > 100MB)
   */
  async multipartUpload(file, options = {}) {
    const partSize = options.partSize || 10 * 1024 * 1024 // デフォルト 10MB/パート
    const parallel = options.parallel || 3 // デフォルト 3 並列

    const credentials = await this.fetchCredentials()
    const fileKey = this._generateFileKey(file, options.directory)

    // 1. マルチパートアップロードを初期化
    const uploadId = await this._initMultipartUpload(
      credentials,
      fileKey,
      file.type
    )

    // 2. パートを計算
    const parts = []
    const totalParts = Math.ceil(file.size / partSize)
    for (let i = 0; i < totalParts; i++) {
      const start = i * partSize
      const end = Math.min(start + partSize, file.size)
      parts.push({
        number: i + 1,
        start,
        end,
        blob: file.slice(start, end)
      })
    }

    // 3. パートをアップロード(並列制御と中断再開付き)
    const uploadedParts = []
    const failedParts = []

    // 中断再開対応:アップロード済みパートをチェック
    if (options.resume) {
      const existingParts = await this._listParts(
        credentials,
        fileKey,
        uploadId
      )
      for (const part of existingParts) {
        uploadedParts.push(part)
      }
    }

    // 未アップロードのパートを抽出
    const pendingParts = parts.filter(
      (p) => !uploadedParts.some((up) => up.partNumber === p.number)
    )

    // 並列アップロード
    const uploadPart = async (part) => {
      try {
        const etag = await this._uploadPart(
          credentials,
          fileKey,
          uploadId,
          part
        )
        return { partNumber: part.number, etag }
      } catch (error) {
        failedParts.push({ part, error })
        throw error
      }
    }

    // Promise.all で並列数を制御
    const chunks = []
    for (let i = 0; i < pendingParts.length; i += parallel) {
      chunks.push(pendingParts.slice(i, i + parallel))
    }

    for (const chunk of chunks) {
      const results = await Promise.allSettled(chunk.map(uploadPart))
      for (const result of results) {
        if (result.status === 'fulfilled') {
          uploadedParts.push(result.value)
        }
      }
    }

    // 全パートがアップロード成功したかチェック
    if (uploadedParts.length !== totalParts) {
      throw new Error(
        `Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
      )
    }

    // 4. マルチパートアップロードを完了(パートを結合)
    await this._completeMultipartUpload(
      credentials,
      fileKey,
      uploadId,
      uploadedParts
    )

    return {
      url: this._getFileUrl(fileKey),
      key: fileKey,
      size: file.size,
      parts: totalParts
    }
  }

  /**
   * ファイル保存パスを生成
   */
  _generateFileKey(file, directory = '') {
    const date = new Date()
    const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
    const random = Math.random().toString(36).substring(2, 10)
    const ext = file.name.split('.').pop() || 'bin'
    const key = directory
      ? `${directory}/${datePath}/${random}.${ext}`
      : `${datePath}/${random}.${ext}`
    return key
  }

  // ============ 各プロバイダー固有メソッド ============

  _getUploadUrl() {
    switch (this.provider) {
      case 'oss':
        return `https://${this.bucket}.oss-${this.region}.aliyuncs.com`
      case 'cos':
        return `https://${this.bucket}.cos.${this.region}.myqcloud.com`
      case 's3':
        return `https://${this.bucket}.s3.${this.region}.amazonaws.com`
      default:
        throw new Error('Unknown provider')
    }
  }

  _getFileUrl(key) {
    return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
      this.provider === 'oss'
        ? 'aliyuncs.com'
        : this.provider === 'cos'
          ? 'myqcloud.com'
          : 'amazonaws.com'
    }/${key}`
  }

  // 各プロバイダーの署名、マルチパートアップロード等のメソッド...(実際の要件に応じて実装)
  _buildFormFields(credentials, fileKey, fileType, options) {
    // 各プロバイダーのフォームフィールド構築ロジック
    // ここでは具体的なプロバイダーのドキュメントに従って実装する必要あり
    return {}
  }

  async _initMultipartUpload(credentials, fileKey, fileType) {
    // 各プロバイダーのマルチパートアップロード初期化ロジック
    return 'upload-id'
  }

  async _uploadPart(credentials, fileKey, uploadId, part) {
    // 各プロバイダーのパートアップロードロジック
    return 'etag'
  }

  async _completeMultipartUpload(credentials, fileKey, uploadId, parts) {
    // 各プロバイダーのマルチパートアップロード完了ロジック
  }

  async _listParts(credentials, fileKey, uploadId) {
    // 各プロバイダーのアップロード済みパート一覧ロジック
    return []
  }
}

// 使用例
const uploader = new DirectUploader({
  provider: 'oss',
  region: 'cn-beijing',
  bucket: 'myapp-images-prod',
  getCredentials: async () => {
    // バックエンドに一時認証情報を申請
    const res = await fetch('/api/upload/credentials')
    return res.json()
  }
})

// 小さなファイルのアップロード
async function uploadAvatar(file) {
  try {
    const result = await uploader.upload(file, {
      directory: 'avatars',
      onProgress: (progress) => {
        console.log(`アップロード進捗: ${progress.percent}%`)
      }
    })
    console.log('アップロード成功:', result.url)
    return result
  } catch (error) {
    console.error('アップロード失敗:', error)
    throw error
  }
}

// 大容量ファイルのマルチパートアップロード
async function uploadVideo(file) {
  try {
    const result = await uploader.multipartUpload(file, {
      directory: 'videos',
      partSize: 10 * 1024 * 1024, // 10MB 毎パート
      parallel: 3, // 3 並列
      resume: true, // 中断再開対応
      onProgress: (progress) => {
        console.log(
          `アップロード進捗: ${progress.percent}%, 転送済み ${progress.loaded}/${progress.total}`
        )
      },
      onPartComplete: (part) => {
        console.log(`パート ${part.number} アップロード完了`)
      }
    })
    console.log('アップロード成功:', result.url)
    return result
  } catch (error) {
    console.error('アップロード失敗:', error)
    // ここでリトライロジックや中断情報の保存を実装可能
    throw error
  }
}

9.2 バックエンド一時認証情報サービス(Node.js/Express)

javascript
/**
 * オブジェクトストレージ STS 一時認証情報サービス
 * 対応:Alibaba Cloud OSS、Tencent Cloud COS、AWS S3
 */
const express = require('express')
const STS = require('ali-oss').STS // Alibaba Cloud
// const COS = require('cos-nodejs-sdk-v5') // Tencent Cloud
const router = express.Router()

// 設定
const config = {
  // Alibaba Cloud OSS 設定
  oss: {
    accessKeyId: process.env.OSS_ACCESS_KEY_ID,
    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
    region: 'oss-cn-beijing',
    bucket: 'myapp-images-prod',
    // STS ロール ARN(RAM 管理画面で作成が必要)
    roleArn: process.env.OSS_STS_ROLE_ARN
  }
}

/**
 * STS 一時認証情報を取得(Alibaba Cloud OSS)
 * POST /api/upload/credentials
 */
router.post('/credentials', async (req, res) => {
  try {
    // 1. ユーザー ID を検証(実際の状況に応じて実装)
    const userId = req.user?.id
    if (!userId) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    // 2. 一意のファイルパスプレフィックスを生成(権限分離用)
    const date = new Date()
    const prefix = `uploads/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${userId}/`

    // 3. STS クライアントを作成
    const sts = new STS({
      accessKeyId: config.oss.accessKeyId,
      accessKeySecret: config.oss.accessKeySecret
    })

    // 4. 一時認証情報を申請
    const result = await sts.assumeRole(
      config.oss.roleArn,
      {
        // Policy で権限範囲を制限(最小権限の原則)
        Statement: [
          {
            Effect: 'Allow',
            Action: [
              'oss:PutObject',
              'oss:InitiateMultipartUpload',
              'oss:UploadPart',
              'oss:CompleteMultipartUpload',
              'oss:AbortMultipartUpload',
              'oss:ListParts'
            ],
            Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
          }
        ],
        Version: '1'
      },
      3600, // 認証情報の有効期限 1 時間
      'web-upload-session-' + Date.now()
    )

    // 5. 認証情報と設定を返す
    res.json({
      success: true,
      data: {
        // STS 一時認証情報
        credentials: {
          accessKeyId: result.credentials.AccessKeyId,
          accessKeySecret: result.credentials.AccessKeySecret,
          sessionToken: result.credentials.SecurityToken,
          expiration: result.credentials.Expiration
        },
        // アップロード設定
        config: {
          provider: 'oss',
          region: config.oss.region,
          bucket: config.oss.bucket,
          endpoint: `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com`,
          prefix: prefix, // ファイルパスプレフィックス
          // セキュリティ制限
          maxSize: 100 * 1024 * 1024, // 最大 100MB
          allowedTypes: [
            'image/jpeg',
            'image/png',
            'image/gif',
            'image/webp',
            'video/mp4'
          ]
        }
      }
    })
  } catch (error) {
    console.error('Get credentials failed:', error)
    res.status(500).json({
      success: false,
      error: 'Failed to get upload credentials',
      message: error.message
    })
  }
})

/**
 * コールバック通知:フロントエンドのアップロード完了後にバックエンドに通知
 * POST /api/upload/callback
 */
router.post('/callback', async (req, res) => {
  try {
    const { key, etag, size, mimeType, originalName } = req.body
    const userId = req.user?.id

    // 1. ファイルの存在を検証
    // 2. ファイル情報をデータベースに保存
    const fileRecord = await db.files.create({
      userId,
      key,
      etag,
      size,
      mimeType,
      originalName,
      url: `https://cdn.example.com/${key}`,
      createdAt: new Date()
    })

    // 3. 非同期処理:サムネイル生成、メタデータ抽出、コンテンツ審査等
    await processFileAsync(fileRecord)

    res.json({
      success: true,
      data: {
        fileId: fileRecord.id,
        url: fileRecord.url,
        size: fileRecord.size
      }
    })
  } catch (error) {
    console.error('Upload callback failed:', error)
    res.status(500).json({
      success: false,
      error: 'Failed to process uploaded file'
    })
  }
})

module.exports = router

9.3 不正リンク防止とセキュリティ設定

javascript
/**
 * CDN 不正リンク防止とセキュリティ設定例
 */

// 1. Referer 不正リンク防止(他のサイトがあなたのリソースを直接参照するのを防ぐ)
const refererConfig = {
  // ホワイトリストモード:以下の Referer のみアクセス許可
  allowList: [
    '*.myapp.com', // メインサイト
    '*.myapp.cn', // 国内サイト
    'localhost:*', // ローカル開発
    '127.0.0.1:*'
  ],

  // ブラックリストモード(オプション):以下の Referer を禁止
  blockList: [
    '*.competitor.com', // 競合他社
    'spam-site.com'
  ],

  // 空 Referer 処理:直接アクセスを許可するか(ブラウザアドレスバーに URL を入力)
  allowEmptyReferer: false // 本番環境では false 推奨、テスト環境では true 可
}

// 2. URL 認証(より安全な不正リンク防止、タイムスタンプと署名付き)
class URLAuth {
  constructor(config) {
    this.key = config.key // 認証鍵、サーバー側のみで保存
    this.expireTime = config.expireTime || 3600 // デフォルト 1 時間有効
  }

  /**
   * 認証付き URL を生成
   * @param {string} url - 元の URL、例 https://cdn.example.com/images/photo.jpg
   * @param {number} expireIn - 有効期限(秒)
   * @returns {string} 認証パラメータ付き URL
   */
  sign(url, expireIn = this.expireTime) {
    const urlObj = new URL(url)
    const pathname = urlObj.pathname
    const timestamp = Math.floor(Date.now() / 1000) + expireIn

    // 署名文字列を構築(プロバイダーによってフォーマットが異なる、これは汎用例)
    const signStr = `${pathname}-${timestamp}-${this.key}`
    const signature = this._md5(signStr)

    // 認証パラメータを追加
    urlObj.searchParams.set('sign', signature)
    urlObj.searchParams.set('t', timestamp.toString())

    return urlObj.toString()
  }

  /**
   * URL 署名を検証(CDN エッジまたはオリジンで使用)
   */
  verify(url) {
    const urlObj = new URL(url)
    const signature = urlObj.searchParams.get('sign')
    const timestamp = parseInt(urlObj.searchParams.get('t'))
    const pathname = urlObj.pathname

    // 期限切れチェック
    if (timestamp < Math.floor(Date.now() / 1000)) {
      return { valid: false, error: 'URL expired' }
    }

    // 署名を検証
    const signStr = `${pathname}-${timestamp}-${this.key}`
    const expectedSign = this._md5(signStr)

    if (signature !== expectedSign) {
      return { valid: false, error: 'Invalid signature' }
    }

    return { valid: true }
  }

  _md5(str) {
    // 実際のプロジェクトでは crypto-js または他の MD5 ライブラリを使用
    // ここでは例示のみ
    return require('crypto').createHash('md5').update(str).digest('hex')
  }
}

// 使用例
const auth = new URLAuth({
  key: 'your-secret-key-only-known-by-server',
  expireTime: 3600 // 1 時間有効
})

// サーバー側で署名付き URL を生成
const signedUrl = auth.sign(
  'https://cdn.example.com/private/document.pdf',
  7200
)
// 結果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456

// CDN エッジまたはオリジンで検証
const result = auth.verify(signedUrl)
if (!result.valid) {
  // 403 Forbidden を返す
}

// 3. IP ホワイトリスト/ブラックリスト
const ipConfig = {
  // 特定 IP のみアクセス許可(内部システム向け)
  whiteList: [
    '192.168.1.0/24', // 内部ネットワークセグメント
    '10.0.0.0/8'
  ],

  // 特定 IP のアクセス禁止(攻撃者をブロック)
  blackList: ['1.2.3.4', '5.6.7.8']
}

// 4. UA(User-Agent)ホワイトリスト/ブラックリスト
const uaConfig = {
  // クローラー/ダウンロードツールを禁止
  blackList: [
    'Wget',
    'curl',
    'python-requests',
    'Scrapy',
    'AhrefsBot',
    'SemrushBot'
  ],

  // ブラウザのみアクセス許可(厳格モード)
  whiteList: [
    'Mozilla/*', // モダンブラウザ
    'AppleWebKit/*'
  ]
}

10. 用語対照表

英語用語日本語対照説明
Object Storageオブジェクトストレージデータをオブジェクトとして管理するデータストレージアーキテクチャ。ファイルシステムの階層構造ではなく、画像、動画、バックアップなどの非構造化データの保存に適している。
Bucketバケットオブジェクトストレージのトップレベルコンテナ。データの整理と分離に使用。各バケットに独立した権限制御と設定がある。
Objectオブジェクト/ファイルオブジェクトオブジェクトストレージの基本単位。データ本体、メタデータ(Metadata)、グローバル一意キー(Key)を含む。
CDNコンテンツ配信ネットワークContent Delivery Network。世界中にエッジノードを配置し、ウェブサイトのコンテンツをユーザーに最も近い場所にキャッシュしてアクセス速度を向上させる。
Edge NodeエッジノードCDN ネットワークで各地に配置されたキャッシュサーバー。ユーザーに直接コンテンツアクセスサービスを提供。
OriginオリジンサーバーCDN がバックトゥオリジンでコンテンツを取得するサーバー。オブジェクトストレージ、ECS、または自社サーバーが利用可能。
Cache Hitキャッシュヒットユーザーがリクエストしたコンテンツが CDN エッジノードに既に存在し、直接返されること。バックトゥオリジン不要。
Cache Missキャッシュミスエッジノードにリクエストされたコンテンツがなく、バックトゥオリジンが必要な状態。
Hit Ratioヒット率キャッシュヒット回数が総リクエスト回数に占める割合。ヒット率が高いほど、バックトゥオリジンが少なく、コストが低い。
TTL生存時間/キャッシュ時間Time To Live。コンテンツが CDN ノードにキャッシュされる有効期間。期限切れ後は再バックトゥオリジンが必要。
Back to SourceバックトゥオリジンCDN エッジノードがオリジンサーバーにコンテンツをリクエストするプロセス。
Purge/RefreshキャッシュパージCDN キャッシュを強制的に無効化し、次回リクエストでバックトゥオリジンして最新コンテンツを取得させる。
Preheatプリフェッチ正式公開前に、能動的にコンテンツを CDN ノードにプッシュし、ユーザーの初回アクセスでキャッシュヒットさせる。
CORSクロスオリジンリソース共有Cross-Origin Resource Sharing。ブラウザのセキュリティ機構で、異なるドメイン間のリソースアクセスを制御。
Referer参照元ページHTTP リクエストヘッダーフィールド。リクエストがどのページからリンクされたかを示す。不正リンク防止に使用。
STSセキュリティトークンサービスSecurity Token Service。一時アクセス認証情報を発行するサービス。フロントエンド直接アップロードなどのシーンで使用。
Multipart Uploadマルチパートアップロード大容量ファイルを複数の小さなパートに分割して並列アップロード。中断再開に対応し、アップロード効率と信頼性を向上。
ETagエンティティタグHTTP レスポンスヘッダー。リソースの特定バージョンを識別するために使用され、キャッシュ検証でよく使われる。
S3 APIS3 互換インターフェースAWS S3 のオブジェクトストレージ API 仕様。多くのクラウドプロバイダーのオブジェクトストレージがこのインターフェースに互換。
Canonical Query String正規クエリ文字列署名文字列の一部で、リクエスト署名の計算に使用され、リクエストの改ざんを防止。

まとめ:オブジェクトストレージ + CDN の黄金律

  1. アップロードは直接方式で:大容量ファイルはマルチパートで、セキュリティは STS で
  2. キャッシュは階層化:ブラウザ → CDN → オリジン、層ごとにキャッシュ
  3. ユーザーに近いサービスを:インテリジェント DNS + グローバルノードカバレッジ
  4. セキュリティを怠らない:HTTPS + 不正リンク防止 + アクセス制御
  5. コストを監視する:ヒット率、帯域幅、ストレージ階層化、継続的最適化

このアーキテクチャはインターネット上のほとんどの静的リソースアクセスを支えています。これを理解すれば、モダン Web パフォーマンス最適化の基盤を理解したことになります。