🌎
CloudFrontのビューワーリクエストに含まれるヘッダーをCloudFront Functionsでレスポンスヘッダーに追加する
What
CloudFrontがビューワーからリクエストを受け取りオリジンに転送する際に、デバイスタイプや位置情報を特定するヘッダーを追加できる。
ユースケースによってはこの情報だけ欲しいというパターンもある。
オリジンはなんでもいいので、適当なコンテンツを配置し、ビューワーリクエストでCloudFrontが追加したヘッダーをレスポンスヘッダーに追加する処理をCloudFront Functionsで実装する。
ビューワーリクエストとビューワーレスポンスについては以下。
今回はオリジンリクエストとオリジンレスポンスは操作しないので、Lambda@EdgeではなくCloudFront Functionsを用いるのがよい。
(CloudFront Functionsの方が機能的な制約は多いが、安くて制限が緩いので。)
How
CloudFront Functionsのコードはこれ
function handler(event) {
var response = event.response;
var headers = response.headers;
var request = event.request;
var viewerInfoHeaders = [
"cloudfront-viewer-city",
"cloudfront-viewer-country",
"cloudfront-viewer-country-name",
'cloudfront-viewer-country-region',
'cloudfront-viewer-country-region-name',
'cloudfront-viewer-latitude',
'cloudfront-viewer-longitude',
'cloudfront-viewer-metro-code',
'cloudfront-viewer-postal-code',
'cloudfront-viewer-time-zone'
];
viewerInfoHeaders.forEach(function(header) {
if (request.headers[header]) {
headers['x-' + header] = {value: request.headers[header].value};
}
});
return response;
}
Terraformのサンプルコードを載せる。
resource "aws_cloudfront_origin_access_identity" "region" {
comment = "origin access identity for region s3 bucket"
}
resource "aws_cloudfront_distribution" "region" {
comment = "cloudfront distribution for region bucket"
enabled = true
aliases = ["region.example.com"]
default_root_object = "index.json"
origin {
domain_name = aws_s3_bucket.region.bucket_regional_domain_name
origin_id = "region"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.region.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
target_origin_id = "region"
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
forwarded_values {
query_string = false
headers = [
"CloudFront-Viewer-Country",
"CloudFront-Viewer-City",
"CloudFront-Viewer-Country-Name",
"CloudFront-Viewer-Country-Region",
"CloudFront-Viewer-Country-Region-Name",
"CloudFront-Viewer-Latitude",
"CloudFront-Viewer-Longitude",
"CloudFront-Viewer-Metro-Code",
"CloudFront-Viewer-Postal-Code",
"CloudFront-Viewer-Time-Zone"
]
cookies {
forward = "none"
}
}
function_association {
event_type = "viewer-response"
function_arn = aws_cloudfront_function.add_viewer_info_headers.arn
}
}
price_class = "PriceClass_200"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.example_com_us_east_1.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
logging_config {
bucket = aws_s3_bucket.log.bucket_regional_domain_name
include_cookies = false
prefix = "log/cloudfront/${aws_s3_bucket.region.bucket_regional_domain_name}"
}
}
resource "aws_cloudfront_function" "add_viewer_info_headers" {
name = "add-viewer-info-headers"
runtime = "cloudfront-js-1.0"
comment = "ビューワーの地理情報をレスポンスヘッダーに追加"
publish = true
code = <<-EOT
function handler(event) {
var response = event.response;
var headers = response.headers;
var request = event.request;
var viewerInfoHeaders = [
"cloudfront-viewer-city",
"cloudfront-viewer-country",
"cloudfront-viewer-country-name",
'cloudfront-viewer-country-region',
'cloudfront-viewer-country-region-name',
'cloudfront-viewer-latitude',
'cloudfront-viewer-longitude',
'cloudfront-viewer-metro-code',
'cloudfront-viewer-postal-code',
'cloudfront-viewer-time-zone'
];
viewerInfoHeaders.forEach(function(header) {
if (request.headers[header]) {
headers['x-' + header] = {value: request.headers[header].value};
}
});
return response;
}
EOT
}
このCloudFrontを叩くと、こんな感じのレスポンスが返ってくる。
$ curl -I https://region.example.com
HTTP/2 200
content-type: application/json
~中略~
x-cloudfront-viewer-city: Tokyo
x-cloudfront-viewer-country: JP
x-cloudfront-viewer-country-name: Japan
x-cloudfront-viewer-country-region: 14
x-cloudfront-viewer-country-region-name: Tokyo
x-cloudfront-viewer-latitude: 12.12345
x-cloudfront-viewer-longitude: 12.12345
x-cloudfront-viewer-postal-code: 123-1234
x-cloudfront-viewer-time-zone: Asia/Tokyo
おまけ: S3の例
適当なコンテンツのTerraformのサンプルコードも載せておく。
resource "aws_s3_bucket" "region" {
bucket = "region"
}
resource "aws_s3_bucket_policy" "region" {
bucket = aws_s3_bucket.region.id
policy = data.aws_iam_policy_document.region.json
}
data "aws_iam_policy_document" "region" {
# NOTE: HTTPSのみ許可する
statement {
sid = "AllowSSLRequestsOnly"
effect = "Deny"
principals {
type = "*"
identifiers = ["*"]
}
actions = ["s3:*"]
resources = [
aws_s3_bucket.region.arn,
"${aws_s3_bucket.region.arn}/*",
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
# NOTE: CFからのアクセスを許可する
statement {
sid = "AllowCloudFrontAccess"
effect = "Allow"
actions = ["s3:GetObject"]
resources = [
aws_s3_bucket.region.arn,
"${aws_s3_bucket.region.arn}/*",
]
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.region.iam_arn]
}
}
}
# NOTE: 意図しない公開を防ぐための設定
resource "aws_s3_bucket_public_access_block" "region" {
bucket = aws_s3_bucket.region.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "region" {
bucket = aws_s3_bucket.region.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_versioning" "region" {
bucket = aws_s3_bucket.region.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "region" {
bucket = aws_s3_bucket.region.id
rule {
id = "expire-noncurrent-version"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 7
}
}
}
# NOTE: ACLを無効にするための設定(バケットポリシーのみでアクセスコントロールする)
resource "aws_s3_bucket_ownership_controls" "region" {
bucket = aws_s3_bucket.region.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_logging" "region" {
bucket = aws_s3_bucket.region.id
target_bucket = aws_s3_bucket.log.id
target_prefix = "log/s3/${aws_s3_bucket.region.bucket}"
}
resource "aws_s3_object" "region_root_object" {
bucket = aws_s3_bucket.region.bucket
key = "index.json"
content_type = "application/json"
content = jsonencode({ status = "OK" })
}
Discussion