Cloud CDNを使用して静的ファイルの署名付きURL配信システムを構築してみた
はじめに
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 を使用するほうが料金を抑えられると考えられます。
シーケンス図
インフラ
インフラはすべて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}`;
}
参考
Discussion