オブジェクトストレージと CDN
💡 学習ガイド:この記事では、ファイルアップロードからユーザーダウンロードまでの完全なフローを解説します。オブジェクトストレージがどのように「スマート倉庫」のように大量のファイルを管理し、CDN がどのように「宅配便ネットワーク」のようにコンテンツをユーザーの目の前に届けるのか、そしてその途中にある「落とし穴」について学びます。基本的な HTTP リクエストと DNS 解決の仕組みを事前に理解しておくことをおすすめします。
始める前に、いくつかの「基礎知識」を補強しておくことをおすすめします:
- HTTP リクエストの流れ:まず ブラウザに URL を入力した後に何が起こるか を読んで、完全なリクエストフローを理解しましょう。
- DNS 解決の仕組み:ドメイン名解決にまだ詳しくない場合は、DNS クエリの流れ の図解パートを先にご覧ください。
0. はじめに:なぜファイルのアップロードとダウンロードはこんなに「遅い」のか?
こんなシーンを想像してみてください:あなたは写真コミュニティに 10MB の高解像度写真をアップロードしましたが、完了するまでに 30 秒もかかりました。一方、北京にいる友達はクリックしてわずか 2 秒でダウンロードできました。同じファイルなのに、なぜアップロードとダウンロードの体験はこれほど違うのでしょうか?
あるいはこう考えてみてください:あなたの EC サイトがダブルイレブン(独身の日)セールを開催し、商品詳細ページに突然数百万のトラフィックが殺到して、サーバーが「ダウン」してしまいました。帯域幅が足りなかったのか?それともアーキテクチャ設計に問題があったのか?
これらの疑問の答えは、すべてオブジェクトストレージと CDN という「黄金のパートナー」コンビに隠されています。
1. オブジェクトストレージ:あなたの「スマートクラウド倉庫」
1.1 オブジェクトストレージとは?
従来のファイルシステムは、あなたの家のクローゼットのようなものです:衣類を「トップス/パンツ/スカート」と階層ごとに整理し、シャツを探すには、クローゼットを開ける→トップスエリア→シャツ棚、という手順を踏みます。この「階層ネスト」モデルは、ファイル数が爆発的に増加すると極めて扱いにくくなります。
一方、オブジェクトストレージは現代の倉庫物流のようなものです:各荷物には一意の「追跡番号」(オブジェクトキー)が振られており、番号を伝えるだけで、倉庫ロボットが膨大な荷物の中から正確に取り出してくれます。
主な違いの一覧:
| 観点 | 従来のファイルシステム | オブジェクトストレージ |
|---|---|---|
| 編成方式 | 階層ディレクトリツリー | フラットなキー・バリュー形式 |
| アクセスプロトコル | POSIX(ローカルファイル操作) | HTTP/REST API |
| 拡張性 | 単一マシンの容量に制限あり | ほぼ無限の水平拡張 |
| メタデータ | 基本属性(サイズ、時刻) | 豊富なカスタムメタデータ |
| 一般的なユースケース | ローカルのオフィス文書 | 画像/動画/バックアップ/静的アセット |
1.2 オブジェクトストレージのコア概念
バケット(Bucket):あなたの「倉庫区画」
バケットはオブジェクトストレージのトップレベルコンテナで、独立した名前空間に相当します。すべてのオブジェクトは必ずいずれかのバケットに格納されます。
命名規則(Alibaba Cloud OSS を例に):
- グローバルで一意:クラウドプロバイダー全体の全ユーザー間で重複不可
- 小文字アルファベット、数字、ハイフンのみ使用可能
- 小文字アルファベットまたは数字で始まり、終わる必要あり
- 長さは 3〜63 文字
実践での落とし穴:かつてあるチームが事業ラインごとに数十個のバケットを作成したところ、月末の請求書を見て愕然としました——各バケットに最低ストレージ料金とリクエスト料金がかかっていたのです。アドバイス:「環境+用途」の組み合わせでバケットを計画しましょう。例:prod-static-assets、dev-backup-archive。
オブジェクト(Object):あなたの「データの荷物」
オブジェクトはストレージの基本単位で、3 つの部分から構成されます:
キー(Key):オブジェクトの一意識別子、「追跡番号」に相当
- 例:
images/avatar/2024/user123.jpg - パスのように見えますが、本質的には単なる文字列です
- 例:
データ(Data):オブジェクトのコンテンツ本体
- 任意のバイナリデータが可能
- サイズ制限はクラウドプロバイダーによって異なります(通常、単一オブジェクト 5TB 以内)
メタデータ(Metadata):オブジェクトを説明する付加情報
- システムメタデータ:Content-Type、ETag、Last-Modified など
- カスタムメタデータ:例
x-oss-meta-owner、x-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 のコアバリューです:コンテンツをユーザーにより近づけること。
2.2 CDN のコアアーキテクチャ
エッジノード:ユーザーに最も近い「宅配ステーション」
エッジノードは CDN ネットワークにおいてユーザーに最も近い階層で、通常以下の場所に展開されます:
- 通信事業者のデータセンター(China Unicom/China Telecom/China Mobile)
- 大都市のインターネットエクスチェンジセンター
- 主要な交通ハブ
中国の主要 CDN ノード分布:
- 一線都市:北京、上海、広州、深圳
- 二線都市:杭州、南京、成都、武漢、西安
- 海外:香港、シンガポール、東京、シリコンバレー、フランクフルト
Edge Node Distribution Demo
Shows global CDN edge-node distribution and scheduling strategy.
Edge node distribution demo placeholder - interaction to be implemented
オリジンサーバー:コンテンツの「総合倉庫」
オリジンサーバーは CDN がコンテンツを取得するためにバックトゥオリジンする場所で、以下が利用可能です:
- オブジェクトストレージ(OSS/COS/S3)
- 自社サーバー(ECS/物理マシン)
- ロードバランサー(SLB/CLB)
重要な設定:
- バックトゥオリジン HOST:CDN ノードがオリジンサーバーにアクセスする際に使用するドメイン/IP
- バックトゥオリジンプロトコル:HTTP か HTTPS か
- バックトゥオリジンポート:80、443、またはカスタムポート
中間層ノード:「地域中継センター」
エッジノードとオリジンサーバーの間には、通常 1 層以上の中間ノードがあります:
- 集約ノード:複数のエッジノードからのバックトゥオリジンリクエストを集約し、オリジンサーバーの負荷を軽減
- 地域センター:一大地域のコンテンツ配信とスケジューリングを担当
この階層アーキテクチャの利点:
- オリジンサーバー負荷の軽減:1000 のエッジノードからのリクエストが、オリジンサーバーへは 10 回だけになる可能性も
- ヒット率の向上:人気コンテンツは中間層でインターセプトされ、バックトゥオリジン不要に
- 障害の隔離:特定のリンクに問題が発生しても、自動的に他のパスに切り替え可能
2.3 CDN 高速化の完全な流れ
実際のユーザーリクエストを追跡してみましょう:
Cache policy demo placeholder - interaction to be implemented
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 つの方式
方式 1:クライアント → サーバー → オブジェクトストレージ(従来型)
ブラウザ → あなたのバックエンドサーバー → オブジェクトストレージフロー:
- ユーザーがファイルを選択し、アップロードをクリック
- ファイルがまずあなたのバックエンドサーバーにアップロードされる
- バックエンドが完全なファイルを受信した後、オブジェクトストレージに転送アップロード
- アップロード結果をユーザーに返す
メリット:
- 実装がシンプルで、フロントエンド・バックエンドともに制御しやすい
- バックエンドでファイル検証、フォーマット変換が可能
- 機密操作のログ記録、権限チェックが可能
デメリット:
- 帯域幅の二重消費:ユーザーアップロードで 1 回、サーバー転送でさらにもう 1 回帯域を消費
- サーバー負荷大:大きなファイルは多くのメモリと CPU を消費
- アップロードが遅い:中継が 1 つ増えるため、ユーザーが体感するアップロード時間が長くなる
適用シーン:小さなファイル(<10MB)、バックエンド処理が必要な場合(画像圧縮、ウォーターマーク付与など)、内部管理システム。
方式 2:クライアントからオブジェクトストレージへ直接アップロード(モダン推奨)
ブラウザ ──────→ オブジェクトストレージ
↑
バックエンドは一時認証情報のみ発行フロー:
- ユーザーがファイルを選択、フロントエンドがまずバックエンドに「アップロード認証情報」をリクエスト
- バックエンドがユーザー ID を検証し、オブジェクトストレージサービスに一時 STS 認証情報(有効期限付き)を申請
- バックエンドが一時認証情報をフロントエンドに返す
- フロントエンドが認証情報を持って、直接オブジェクトストレージにファイルをアップロード
- オブジェクトストレージがアップロード結果を返し、フロントエンドがバックエンドに「アップロード完了」を通知
メリット:
- アップロードが速い:中継ステップが減り、ユーザー体感速度が最速
- サーバー負荷が小さい:認証情報発行のみを処理し、ファイルストリームを処理しない
- 帯域幅の節約:アップロードトラフィックが 1 回で済む
- セキュリティが高い:一時認証情報には有効期限があり、漏洩しても被害は限定的
デメリット:
- 実装がやや複雑で、STS、署名メカニズムの理解が必要
- フロントエンドでマルチパートアップロード、中断再開などのロジックを処理する必要あり
- クロスオリジン(CORS)の設定が必要
適用シーン:大きなファイルのアップロード、ユーザー生成コンテンツ(UGC)、高同時実行アップロードが必要なビジネス。
方式 3:マルチパートアップロード+中断再開(大容量ファイル必須)
10GB 動画ファイル
↓
1000 個の 10MB パートに分割
↓
並列アップロード(同時に 5 パート)
↓
ネットワーク断!600 パートまでアップロード済み
↓
ネットワーク復旧、601 パート目から再開
↓
全パート完了、「結合」リクエストを発行なぜマルチパートが必要なのか?
| シーン | 非マルチパート | マルチパート |
|---|---|---|
| ネットワーク変動 | 99% 転送後に切断、全再送 | 失敗したパートのみ再送 |
| アップロード速度 | シングルスレッド、遅い | マルチスレッド並列、速い |
| メモリ使用量 | ファイル全体をキャッシュ必要 | 現在のパートのみキャッシュ |
| 進捗表示 | 0% と 100% のみ | 各パートの進捗を正確に表示 |
主要クラウドプロバイダーのマルチパート仕様:
| プロバイダー | パートサイズ制限 | 最大パート数 | 最小パートサイズ |
|---|---|---|---|
| Alibaba Cloud OSS | 100MB | 10000 | 100KB |
| Tencent Cloud COS | 5GB | 10000 | 1MB |
| AWS S3 | 5GB | 10000 | 5MB(推奨) |
| Qiniu Cloud | 100MB | 10000 | 4MB |
3.2 CDN バックトゥオリジン戦略の詳細
Cache policy demo placeholder - interaction to be implemented
「バックトゥオリジン」とは?
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 demo placeholder - interaction to be implemented
キャッシュキー(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 つの画像が同じファイルとみなされ、混乱が生じます。
解決策:カスタムキャッシュキールール
| ルール | 例 | 効果 |
|---|---|---|
| 指定クエリパラメータを保持 | w、h を保持 | 異なるサイズを別々にキャッシュ |
| 全クエリパラメータを保持 | すべて保持 | 完全な正確一致 |
| 特定のクエリパラメータを無視 | token、timestamp を無視 | タイムスタンプ付き 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 のベストプラクティス:
// 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 scheduling demo placeholder - interaction to be implemented
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 optimization demo placeholder - interaction to be implemented
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、SHA1OCSP 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 analytics demo placeholder - interaction to be implemented
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_status | HTTP ステータスコード | 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 クロスオリジン設定:
<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:ストレージ階層化、自動ライフサイクル管理
# ライフサイクルルール例
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:帯域幅ピークキャップとアラート
# 帯域幅キャップ設定
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)
/**
* オブジェクトストレージ直接アップロードユーティリティクラス
* 対応: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)
/**
* オブジェクトストレージ 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 = router9.3 不正リンク防止とセキュリティ設定
/**
* 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 API | S3 互換インターフェース | AWS S3 のオブジェクトストレージ API 仕様。多くのクラウドプロバイダーのオブジェクトストレージがこのインターフェースに互換。 |
| Canonical Query String | 正規クエリ文字列 | 署名文字列の一部で、リクエスト署名の計算に使用され、リクエストの改ざんを防止。 |
まとめ:オブジェクトストレージ + CDN の黄金律
- アップロードは直接方式で:大容量ファイルはマルチパートで、セキュリティは STS で
- キャッシュは階層化:ブラウザ → CDN → オリジン、層ごとにキャッシュ
- ユーザーに近いサービスを:インテリジェント DNS + グローバルノードカバレッジ
- セキュリティを怠らない:HTTPS + 不正リンク防止 + アクセス制御
- コストを監視する:ヒット率、帯域幅、ストレージ階層化、継続的最適化
このアーキテクチャはインターネット上のほとんどの静的リソースアクセスを支えています。これを理解すれば、モダン Web パフォーマンス最適化の基盤を理解したことになります。