🔐

Cloud CDNを使用して静的ファイルの署名付きURL配信システムを構築してみた

2024/04/29に公開

はじめに

Publicには公開したくない静的ファイル配信する場合、署名付きURLを使うことで、認証されたユーザーのみに配信することができます。
署名付きURLは、URLを知っている全員にリソースを配信することができ、有効期限を設定することで、期限が切れたら配信を停止することができます。

Cloud CDN VS Cloud Storage

静的ファイルを配信する場合、Cloud CDNとCloud Storageのどちらを使うか迷うところです。
どちらも署名付きURLやキャッシュの仕組みがあるため、どちらを使っても実現できますが、処理するデータ量によって料金が異なることや、キャッシュ可能な最大サイズなどの違いがあります。

ユーザーにコンテンツを配信する際に最高のパフォーマンスを得るには、Cloud CDN で Cloud Storage を使用することをおすすめします。

Cloud Storage の組み込みキャッシュと Cloud CDN のどちらを選択するかは毎月処理するデータ量が目安になります。このデータ量によりネットワークの費用が決まります。1 か月間に処理するキャッシュ可能なデータ量が数 GiB 未満の場合は、Cloud Storage の組み込みキャッシュを使用するほうが、全体的な費用が抑えられると考えられます。1 か月あたり 100 GiB 以上のキャッシュ可能なデータを常時配信している場合、またはリクエストごとにロギングとカスタム ヘッダーを使用する必要がある場合は、Cloud CDN を使用するほうが料金を抑えられると考えられます。

https://cloud.google.com/storage/docs/caching?hl=ja

シーケンス図

インフラ

インフラはすべてTerraformを使い構築しました。

グローバルIPアドレス取得

後続でCDNをSSL化するためにグローバルIPアドレスを取得します。

resource "google_compute_global_address" "cdn-ip" {
  name = "cdn-ip"
  project = var.project
}

ドメイン取得

後続のCertificate Managerで証明書を発行するためにドメインを取得します。

今回はサブドメインを取得しました。

resource "google_dns_record_set" "a_cdn" {
  name         = "cdn.${google_dns_managed_zone.dns-zone.dns_name}"
  managed_zone = google_dns_managed_zone.dns-zone.name
  type         = "A"
  ttl          = 300
  rrdatas      = ["IP_ADDRESS"]
}

Cloud Storage バケット作成

Cloud Storageに静的ファイルを配置するためのバケットを作成します。

resource "google_storage_bucket" "static_file" {
  project       = var.project
  name          = "static-file"
  storage_class = "MULTI_REGIONAL"
  location      = "ASIA"
}

Secret Manager シークレット作成

署名付きURL生成に必要なシークレットを作成します。

resource "google_secret_manager_secret" "static_file_secret_key_value" {
  secret_id = "static-file-secret-key-value"

  replication {
    automatic = true
  }
}

# 後続の署名付きURL生成で使用するためシークレットキーの最新バージョンを取得
data "google_secret_manager_secret_version" "static_file_secret_key_value" {
  secret  = google_secret_manager_secret.static_file_secret_key_value.id
  version = "latest"
}

Secret Managerの追加が終わったらGCPコンソールからシークレットの値を設定します。

Cloud CDN 作成

resource "google_storage_bucket_iam_member" "signed-url-user" {
  bucket = google_storage_bucket.static_file.name
  role   = "roles/storage.objectViewer"
  # Cloud CDN作成時に自動作成されるサービスアカウント
  member = "serviceAccount:service-${var.project_number}@cloud-cdn-fill.iam.gserviceaccount.com"

  depends_on = [
    google_compute_backend_bucket_signed_url_key.backend_key
  ]
}

resource "google_compute_backend_bucket" "cdn-backend-bucket" {
  project     = var.project
  name        = "backend-${google_storage_bucket.static_file.name}"
  bucket_name = google_storage_bucket.static_file.name
  enable_cdn  = true // Cloud CDNの有効化
}

resource "google_compute_backend_bucket_signed_url_key" "backend_key" {
  project        = var.project
  name           = "static-file-bucket-key"
  key_value      = data.google_secret_manager_secret_version.static_file_secret_key_value.secret_data
  backend_bucket = google_compute_backend_bucket.cdn-backend-bucket.name
}

resource "google_compute_url_map" "cdn" {
  project         = var.project
  name            = "static-file-cdn"
  default_service = google_compute_backend_bucket.cdn-backend-bucket.id
}

resource "google_compute_target_https_proxy" "cdn-proxy" {
  project          = var.project
  name             = "cdn-proxy"
  url_map          = google_compute_url_map.cdn.id
  ssl_certificates = [google_compute_managed_ssl_certificate.tls.id]
}

resource "google_compute_managed_ssl_certificate" "tls" {
  project = var.project
  name    = "static-file"
  managed {
    domains = ["${var.static_file_domain}"]
  }
}

// HTTPSリダイレクト
resource "google_compute_global_forwarding_rule" "cdn" {
  project    = var.project
  name       = "static-file"
  target     = google_compute_target_https_proxy.cdn-proxy.id
  port_range = "443"
  ip_address = var.static_file_cdn_ip
}

署名付きURL生成

署名付きURL生成の実装は以下のようになります。
今回はNode.jsで実装しました。

import { add, getUnixTime } from 'date-fns';
import { createHmac } from 'crypto';

generateSignUrl(): string {
  // URLの有効期限を1時間に設定
  const unixTimestamp = getUnixTime(add(new Date(), { hours: 1 }));

  const url = process.env.CDN_URL;
  const signSecretKeyName = process.env.CDN_SIGN_SECRET_KEY_NAME;
  const signSecretKeyValue = process.env.CDN_SIGN_SECRET_KEY_VALUE;

  const segments = [
    `?Expires=${unixTimestamp}`,
    `&KeyName=${signSecretKeyName}`,
  ];

  const signURL = `${url}${segments.join('')}`;
  const keyValue = Buffer.from(signSecretKeyValue, 'base64');

  const signature = createHmac('sha1', keyValue)
    .update(signURL)
    .digest('base64')
    // convert url safe
    .replace(/\+/g, '-')
    .replace(/\//g, '_');

  segments.push(`&Signature=${signature}`);

  const singedQuery = segments.join('');

  return `${url}${singedQuery}`;
}

参考

https://cloud.google.com/cdn/docs/using-signed-urls?hl=ja
https://www.holyshared.ninja/entry/2021/07/13/095829

GitHubで編集を提案

Discussion