ちょっと前まで大雪で寒かったのに、もうだいぶ暖かくなってきましたね。花粉が飛散しだすのでは無いかと戦々恐々としています。
さて、サイト移行のときって、だいたいこの2つが同時に来ませんか。
-
ステージングだけBasic認証をかけたい
-
旧URLや旧ドメインからのリダイレクトを大量にさばきたい
今回はこの2つを CloudFront Functions の viewer-request でまとめて処理し、Terraformで管理した構成を紹介します。
やりたいこと
やりたいことはシンプルです。
- Basic認証の判定
- 旧ドメインから正式ドメインへの301
- HTTPからHTTPSへの301
- apexからcanonical hostへの301(`/.well-known/*` は除外)
- KeyValueStoreにあるパスリダイレクトの適用
- .../ を .../index.html に書き換え
この順序にしておくと、無駄なオリジン到達を減らせます。ループの温床も作りにくいです。
構成
- CloudFront Distribution
- CloudFront Function(`viewer-request`)
- CloudFront KeyValueStore(リダイレクト辞書)
- S3
- Terraform module
Terraform側の定義
Function本体はテンプレートに分離し、Terraformから値を注入します。
resource "aws_cloudfront_function" "rewrite_index" {
name = local.rewrite_function_name
runtime = "cloudfront-js-2.0"
comment = "Apply redirects and append index.html"
publish = true
key_value_store_associations = [aws_cloudfront_key_value_store.redirects.arn]
code = templatefile("${path.module}/function.js.tftpl", {
legacy_host_redirect_from = var.legacy_host_redirect_from
legacy_host_redirect_to = var.legacy_host_redirect_to
apex_host = var.apex_host
canonical_host = var.canonical_host
basic_auth_enabled = var.basic_auth_enabled
basic_auth_header = local.basic_auth_authorization_header
})
}
viewer-request に関連付けるのがポイントです。オリジンへ到達する前の段階で判定できます。
default_cache_behavior {
# ...
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.rewrite_index.arn
}
}
Function実装のイメージ
実装イメージはこんな感じです。
import cf from "cloudfront";
const kvs = cf.kvs();
const BASIC_AUTH_ENABLED = true; // Terraformから注入
const BASIC_AUTH_HEADER = "Basic <base64(username:password)>";
function createRedirectResponse(location) {
return {
statusCode: 301,
statusDescription: "Moved Permanently",
headers: {
location: { value: location }
}
};
}
function createUnauthorizedResponse() {
return {
statusCode: 401,
statusDescription: "Unauthorized",
headers: {
"www-authenticate": { value: 'Basic realm="Restricted"' }
}
};
}
async function handler(event) {
const request = event.request;
const uri = request.uri || "/";
// 1) Basic auth
if (BASIC_AUTH_ENABLED) {
const auth = request.headers.authorization?.value || "";
if (auth !== BASIC_AUTH_HEADER) {
return createUnauthorizedResponse();
}
}
// 2-4) host/protocol redirect(省略)
// 5) path redirect by KVS
try {
const target = await kvs.get(uri);
if (target) {
return createRedirectResponse(target);
}
} catch (_e) {
// KVS miss時は通常フローを継続
}
// 6) trailing slash rewrite
if (request.uri.endsWith("/")) {
request.uri += "index.html";
}
return request;
}
リダイレクトは .htaccess を正本にする
既存運用で .htaccess があるなら Redirect permanent を抽出して KVS に流し込む運用が楽です。
locals {
htaccess_content = file(var.htaccess_file_path)
htaccess_redirect_lines = regexall(
"(?m)^Redirect\\s+permanent\\s+(\\S+)\\s+(\\S+)\\s*$",
local.htaccess_content
)
htaccess_redirect_pairs = [
for m in local.htaccess_redirect_lines : {
source = m[0]
target = m[1]
}
]
}
環境差分
同じmoduleを使い、本番環境・ステージング環境の差分は tfvars に寄せます。
# staging
basic_auth_enabled = true
basic_auth_username = ""
basic_auth_password = ""
# production
basic_auth_enabled = false
動作確認
最低限、ここは確認しておくと安心です。
# Basic auth未指定のため401
curl -I https://stg.example.com/
# Basic auth指定あり
curl -I -u ':' https://stg.example.com/
# 旧URLから301
curl -I https://www.legacy.example.com/old-path/
# HTTPからHTTPSへ301
curl -I http://www.example.com/some-path/
注意点
- Basic認証の資格情報は `tfvars` に平文で置かない
- CI/CDのSecret、SSM Parameter Store、Secrets Managerなどから注入する
- 301ルールに循環参照がないか事前チェックする
- Query stringを保持して転送し、トラッキング欠落を防ぐ
まとめ
CloudFront Functionsを viewer-request で使うと、Basic認証、リダイレクト、URL正規化をエッジに寄せて運用できます。
移行フェーズでリダイレクトが多い案件だと、.htaccess を正本にして KVS に反映する構成はかなり扱いやすいです。
Terraformでmodule化しておけば、環境差分の管理も素直にできます。
現場からは以上です。