cdn-setup について
cdn-setup は CDN 設定向けのスキルです。CloudFront と Cloudflare の導入、キャッシュ、トラブルシューティングを扱い、Web サイトや API の配信に利用できます。
主な機能
- 01CloudFront と Cloudflare CDN の設定
- 02キャッシュルールとオリジンヘッダーの設定
- 03パス単位のエッジキャッシュ削除
- 04Terraform による設定管理
- 05キャッシュ状況の確認と問題解決
活用シーン
- 01グローバル向け静的アセットの高速化
- 02CDN での API レスポンスキャッシュ
- 03デプロイ後の古いオブジェクトの削除
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill cdn-setupインストール後、Claude Code で/cdn-setupを入力して読み込まれたか確認します。
詳細
CDN の設定
適切なキャッシュ、無効化、セキュリティを備えたコンテンツ配信ネットワークを設定し、世界中にアセットを高速かつ安定して配信します。
使用する場面
- 静的アセット(JS、CSS、画像、フォント)を低レイテンシで世界中に配信する場合。
- オリジンサーバーからのトラフィックをオフロードして、コンピューティングコストを削減する場合。
- エッジでの TLS 終端と DDoS 保護を追加する場合。
- 地理情報に基づくルーティングやコンテンツ制限を実装する場合。
- エッジキャッシュで API の応答を高速化する場合。
前提条件
- DNS 管理にアクセスできるドメイン。
- 配信するコンテンツを格納したオリジンサーバーまたは S3/R2 バケット。
- 設定済みの AWS CLI(CloudFront 用)。
- ゾーンを設定済みの Cloudflare アカウント(Cloudflare CDN 用)。
- Terraform 1.5 以降(Infrastructure as Code の例用)。
AWS CloudFront
CLI でディストリビューションを作成する
# Create an S3 origin distribution with OAC (Origin Access Control)
aws cloudfront create-distribution --distribution-config '{
"CallerReference": "my-site-'$(date +%s)'",
"Comment": "Production site CDN",
"Enabled": true,
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "s3-origin",
"DomainName": "my-bucket.s3.us-east-1.amazonaws.com",
"OriginPath": "",
"S3OriginConfig": {
"OriginAccessIdentity": ""
},
"OriginAccessControlId": "E2QWRUHAPOMQZL"
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "s3-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true
},
"DefaultRootObject": "index.html",
"PriceClass": "PriceClass_100",
"ViewerCertificate": {
"ACMCertificateArn": "arn:aws:acm:us-east-1:123456789:certificate/abc-123",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
},
"Aliases": {
"Quantity": 1,
"Items": ["www.example.com"]
},
"CustomErrorResponses": {
"Quantity": 1,
"Items": [{
"ErrorCode": 404,
"ResponseCode": "200",
"ResponsePagePath": "/index.html",
"ErrorCachingMinTTL": 10
}]
}
}'
キャッシュの無効化
# Invalidate specific paths
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths "/index.html" "/css/*" "/js/*"
# Invalidate everything (costs apply per path)
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths "/*"
# Check invalidation status
aws cloudfront get-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--id I1A2B3C4D5E6F7
# List recent invalidations
aws cloudfront list-invalidations --distribution-id E1A2B3C4D5E6F7
CloudFront Functions(軽量なエッジロジック)
// URL rewrite function — add index.html to directory requests
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
Terraform を使った CloudFront
# cloudfront.tf
resource "aws_cloudfront_distribution" "site" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
aliases = ["www.example.com"]
price_class = "PriceClass_100"
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "s3-origin"
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3-origin"
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized
origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin
viewer_protocol_policy = "redirect-to-https"
compress = true
}
# SPA fallback
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
# API pass-through (no caching)
ordered_cache_behavior {
path_pattern = "/api/*"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "api-origin"
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # AllViewerExceptHostHeader
viewer_protocol_policy = "https-only"
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
resource "aws_cloudfront_origin_access_control" "oac" {
name = "s3-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
Cloudflare CDN
ゾーンの設定
# Add a zone
curl -X POST "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"example.com","jump_start":true}'
# Get zone ID
ZONE_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=example.com" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result[0].id')
キャッシュルール(Page Rules の代替)
# Create a cache rule for static assets
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_cache_settings/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"expression": "(http.request.uri.path.extension in {\"css\" \"js\" \"png\" \"jpg\" \"woff2\" \"svg\"})",
"action": "set_cache_settings",
"action_parameters": {
"cache": true,
"browser_ttl": { "mode": "override_origin", "default": 2592000 },
"edge_ttl": { "mode": "override_origin", "default": 86400 }
},
"description": "Cache static assets aggressively"
}
]
}'
キャッシュの消去
# Purge everything
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"purge_everything":true}'
# Purge specific URLs
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"files":["https://example.com/style.css","https://example.com/app.js"]}'
# Purge by prefix
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prefixes":["https://example.com/images/"]}'
オリジンのキャッシュヘッダー
nginx のキャッシュヘッダー
# Immutable hashed assets (fingerprinted filenames)
location ~* \.(js|css)$ {
if ($uri ~* "\.[a-f0-9]{8,}\.(js|css)$") {
expires 1y;
add_header Cache-Control "public, immutable";
}
expires 7d;
add_header Cache-Control "public, must-revalidate";
}
# Images and fonts
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# HTML — always revalidate
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# API responses — no caching
location /api/ {
add_header Cache-Control "no-store, no-cache";
add_header Vary "Authorization, Accept";
}
Cache-Control の早見表
| ヘッダー | 意味 |
|---|---|
public, max-age=31536000, immutable | 1 年間キャッシュし、再検証しません(ハッシュ化されたアセット) |
public, max-age=86400, must-revalidate | 1 日間キャッシュし、その後に鮮度を確認します |
private, max-age=600 | ブラウザーのみでキャッシュし、10 分間保持します(ユーザー固有のコンテンツ) |
no-cache | 配信前に必ずオリジンで再検証します |
no-store | キャッシュしません(機密データ) |
s-maxage=3600 | CDN は 1 時間キャッシュします。共有キャッシュでは max-age より優先されます |
キャッシュのウォームアップ
# Warm cache for critical pages after deployment
#!/bin/bash
URLS=(
"https://www.example.com/"
"https://www.example.com/products"
"https://www.example.com/about"
"https://www.example.com/css/main.abc123.css"
"https://www.example.com/js/app.def456.js"
)
for url in "${URLS[@]}"; do
curl -s -o /dev/null -w "%{http_code} %{time_total}s %{url_effective}\n" "$url"
done
キャッシュパフォーマンスの監視
# Check cache status from response headers
curl -sI https://www.example.com/style.css | grep -i -E "cf-cache|x-cache|age|cache-control"
# Expected headers:
# cf-cache-status: HIT (Cloudflare)
# x-cache: Hit from cloudfront (CloudFront)
# age: 3600 (seconds since cached)
# CloudFront cache hit ratio
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1A2B3C4D5E6F7 \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 300 \
--statistics Average
トラブルシューティング
| 症状 | 原因 | 対処方法 |
|---|---|---|
cf-cache-status: DYNAMIC | キャッシュルールに一致しないか、Cache-Control によってキャッシュが妨げられています | s-maxage を設定するか、そのパスに対するキャッシュルールを作成します |
| キャッシュヒット率が 50% 未満 | TTL が短いか、URL の種類が多すぎます(クエリ文字列) | TTL を延ばし、キャッシュキーから不要なクエリ文字列を除外します |
| デプロイ後に古いコンテンツが表示される | 古いオブジェクトがエッジにキャッシュされたままです | キャッシュを無効化します。コンテンツハッシュ付きのファイル名を使えば、この問題を完全に回避できます |
| CDN 経由で CORS エラーが発生する | CDN が誤った Vary ヘッダーを削除またはキャッシュしています | Vary: Origin を追加し、Origin を転送するようオリジンリクエストポリシーを設定します |
| CDN から 502 エラーが返される | オリジンが停止しているか、タイムアウトしています | オリジンの状態を確認し、CDN のオリジンタイムアウト設定を延長します |
| 混在コンテンツの警告が表示される | CDN は HTTPS で配信していますが、オリジンのリンクでは HTTP が使われています | viewer-protocol-policy: redirect-to-https を設定し、オリジンの URL を修正します |
| キャッシュ無効化のコストが高い | デプロイのたびに /* をパージしています | フィンガープリント付きのファイル名を使い、index.html のみを無効化します |
関連するスキル
- DNS 管理 - CDN の CNAME 設定に使用する DNS レコード
- cloudflare-pages - Pages プロジェクト向けの Cloudflare 組み込み CDN
- リバースプロキシ - CDN の背後にあるオリジンサーバーの設定
- 負荷分散 - 複数オリジンの CDN バックエンド
コメント
Markdown 対応