AWS Route53 + Application Load Balancer環境にCloudFrontを導入する

はじめに

こんにちは!Product Unit SINIS for X 開発チームの西野(@fingerEase24)です。

今回はAWSのRoute53 + Application Load Balancer(ALB)環境で構築されたAPIサーバーにCloudFrontを導入する作業を行うにあたっての流れや躓きポイントについて説明します。

なお、インフラ環境についてはTerraformによるIaC管理を行っています。

イントロダクション

想定読者

  • AWSでのインフラ構築経験がある方
  • TerraformやCloudFormationなどによるIaC管理を行ったことがある方

なぜCloudFrontの導入に至ったか

きっかけはXの以下の投稿でした。

本記事のケースではAPIサーバーというユースケース上、キャッシュは無効化しておりキャッシュ戦略によるCDNの恩恵は受けていませんが、その場合でもCloudFrontの導入によってエッジロケーションからのアクセスによる通信の高速化や、無料枠によるネットワーク転送料削減などのメリットが生じます。

また、オリジンサーバーの前にリバースプロキシとしてCloudFrontを挟むことにより、セキュリティの向上も見込めます。

導入手順

導入にあたって、以下の手順で作業を行いました。

  1. 導入前ベンチマーク
  2. バージニア北部でACM証明書を取得(Terraform管理外)
  3. CloudFrontロギング用S3バケットの設定
  4. CloudFrontディストリビューションの作成
  5. ALBにHTTPリスナーを追加
  6. CloudFront疎通確認
  7. Athenaからのログ参照設定
  8. 運用開始(Route53の向き先をALBからCloudFrontに変更)
  9. 導入後ベンチマーク
  10. 事後作業

順に解説していきます。

導入前ベンチマーク

直接的に必要な作業ではありませんが、CloudFront導入による効果を測定するために導入前後でsiegeを用いて以下の条件でヘルスチェックのエンドポイントを対象にベンチマークを実施しました。

  • ユーザー数: 10
  • 継続時間: 1分
  • リクエスト間隔: 2秒おき

結果は以下のようになりました。

Lifting the server siege...
Transactions:                458 hits
Availability:             100.00 %
Elapsed time:              60.38 secs
Data transferred:           0.01 MB
Response time:              0.32 secs
Transaction rate:           7.59 trans/sec
Throughput:             0.00 MB/sec
Concurrency:                2.44
Successful transactions:         458
Failed transactions:               0
Longest transaction:            1.63
Shortest transaction:           0.11

バージニア北部でACM証明書を取得(Terraform管理外)

CloudFrontディストリビューションで利用するための証明書を発行するのですが、CloudFrontでは利用できる証明書のリージョンがバージニア北部(us-east-1)に限定されています。

docs.aws.amazon.com

そのため、ここはTerraform管理外として手動で作成しました。

AWS Certificate Manager(ACM)にバージニア北部リージョンでアクセスし、「証明書をリクエスト」から証明書のリクエストを行います。

証明書タイプは「パブリック証明書をリクエスト」を選択し、次へ進みます。

ドメイン名には現在のAPIサーバーのドメインを入力し、検証方法は「DNS検証」を指定します。

キーアルゴリズムには現在のシステム構成とサービス要件にもよりますが、可能であれば RSA 2048 よりも暗号化強度の高い ECDSA P 256 を選ぶと良いでしょう。

なお、現状CloudFrontは ECDSA P 384 には対応していないようです。

以上でリクエストを行います。

CloudFrontロギング用S3バケットの設定

ここからTerraform管理による構築を行います。

まずはCloudFrontからロギングを行うための準備をします。

CloudFrontからのS3へのロギングを行うためにはバケットACLを有効にする必要があるのでその設定を追加します。

docs.aws.amazon.com

# バケット ACL に付与する CloudFront の被付与者 ID
data "aws_cloudfront_log_delivery_canonical_user_id" "cloudfront" {}

# 現在の AWS アカウントの被付与者 ID
data "aws_canonical_user_id" "current" {}

resource "aws_s3_bucket" "example_private" {
  bucket = "example-prod-private"
}

resource "aws_s3_bucket_acl" "example_private" {
  depends_on = [aws_s3_bucket_ownership_controls.example_private]

  bucket = aws_s3_bucket.example_private.id

  access_control_policy {
    grant {
      grantee {
        id   = data.aws_canonical_user_id.current.id
        type = "CanonicalUser"
      }
      permission = "FULL_CONTROL"
    }

    grant {
      grantee {
        id   = data.aws_cloudfront_log_delivery_canonical_user_id.cloudfront.id
        type = "CanonicalUser"
      }
      permission = "FULL_CONTROL"
    }

    owner {
      id = data.aws_canonical_user_id.current.id
    }
  }
}

この設定を行うことで、対象のS3バケットについて外部アカウント c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0 にアクセス権が付与されます。

CloudFrontディストリビューションの作成

前項までで用意したACM証明書、ロギング用S3バケットを用いてディストリビューションの作成を行います。

resource "aws_cloudfront_distribution" "example_cloudfront" {
  origin {
    domain_name = aws_lb.example_api.dns_name
    origin_id   = aws_lb.example_api.dns_name

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  enabled         = true
  is_ipv6_enabled = true
  aliases         = ["api.example.com"]

  default_cache_behavior {
    target_origin_id         = aws_lb.example_api.dns_name
    viewer_protocol_policy   = "https-only"
    allowed_methods          = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods           = ["GET", "HEAD"]
    cache_policy_id          = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html?icmpid=docs_cf_help_panel#managed-cache-policy-caching-disabled
    origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html?icmpid=docs_cf_help_panel#managed-origin-request-policy-all-viewer
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  logging_config {
    bucket = aws_s3_bucket.example_private.bucket_domain_name
    prefix = "cloudfront-example-api/"
  }

  viewer_certificate {
    acm_certificate_arn      = "arn:aws:acm:us-east-1:<account-id>:certificate/<certificate-id>" # ACM証明書のリージョンがバージニア北部のためTerraform管理外
    minimum_protocol_version = "TLSv1.2_2021"
    ssl_support_method       = "sni-only"
  }
}

各設定について説明します。

origin_id   = aws_lb.example_api.dns_name

オリジンサーバーには現在稼働中のALBを指定します。

custom_origin_config {
  http_port              = 80
  https_port             = 443
  origin_protocol_policy = "http-only"
  origin_ssl_protocols   = ["TLSv1.2"]
}

CloudFrontとオリジンサーバー(今回の場合はALB)間は内部通信になるため、HTTPで行っています。

セキュリティ要件が厳しい場合にはこの間の通信もHTTPSにすることを検討すると良さそうです。

default_cache_behavior {
  target_origin_id         = aws_lb.example_api.dns_name
  viewer_protocol_policy   = "https-only"
  allowed_methods          = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
  cached_methods           = ["GET", "HEAD"]
  cache_policy_id          = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html?icmpid=docs_cf_help_panel#managed-cache-policy-caching-disabled
  origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html?icmpid=docs_cf_help_panel#managed-origin-request-policy-all-viewer
}

クライアントとCloudFront間の通信はHTTPSにします。

キャッシュポリシーとオリジンリクエストポリシーについてはどちらもマネージドポリシーを利用しています。

キャッシュについてはAPIサーバーであることと現状を鑑みて今回は最適化は行わず、無効化のマネージドポリシーを選択しています。

パフォーマンス要件がネックになっていたり重視されたりする場合はキャッシュの有効化も検討すると良いでしょう。

viewer_certificate {
  acm_certificate_arn      = "arn:aws:acm:us-east-1:<account-id>:certificate/<certificate-id>" # ACM証明書のリージョンがバージニア北部のためTerraform管理外
  minimum_protocol_version = "TLSv1.2_2021"
  ssl_support_method       = "sni-only"
}

証明書についてはTerraform管理外のため、先ほど作成したACM証明書のARNを直接指定しています。

また、こちらは任意ですが、別途リアルタイムメトリクスの設定も追加しました。

resource "aws_cloudfront_monitoring_subscription" "example_cloudfront_monitoring" {
  distribution_id = aws_cloudfront_distribution.example_cloudfront.id

  monitoring_subscription {
    realtime_metrics_subscription_config {
      realtime_metrics_subscription_status = "Enabled"
    }
  }
}

これにより、レイテンシーのモニタリングなどが有効になります。

詳しくは以下をご覧ください。

docs.aws.amazon.com

ALBにHTTPリスナーを追加

CloudFront-ALB間の通信をHTTPで行うにあたり、ALBへのHTTPリスナーの追加が必要となるので以下の変更を加えます。

resource "aws_lb_listener" "example_api_http" {
  load_balancer_arn = aws_lb.example_api.arn

  protocol = "HTTP"
  port     = 80

  default_action {
    target_group_arn = aws_lb_target_group.example_api_blue.arn
    type             = "forward"
  }

  lifecycle {
    ignore_changes = [
      default_action
    ]
  }
}

CloudFront疎通確認

上記設定を反映し、ディストリビューションが作成されたら直接ディストリビューションドメインに向けてリクエストを送り、疎通確認を行います。

試しにWelcomeという文字列を返すヘルスチェック用のエンドポイントにリクエストを送ってみました。

❯ curl --location 'https://<distribution-domain-name>/'
Welcome

問題なく動作することが確認できればOKです。

Athenaからのログ参照設定

ロギング用S3バケットに出力されたログについて、Athenaを利用してログ解析を行うための準備をしておきます。

ここでは手順の詳細は省きますが、以下の公式記事が参考になります。

docs.aws.amazon.com

運用開始(Route53の向き先をALBからCloudFrontに変更)

これで準備が整ったので、Route53の向き先を変更してALBの前にCloudFrontを挟むようにします。

以下のように変更します。

resource "aws_route53_record" "example_api" {
  (省略)

  alias {
-   name                   = aws_lb.example_api.dns_name
-   zone_id                = aws_lb.example_api.zone_id
+   name                   = aws_cloudfront_distribution.example_cloudfront.domain_name
+   zone_id                = aws_cloudfront_distribution.example_cloudfront.hosted_zone_id
    evaluate_target_health = false
  }
}

なお、Blue/Greenデプロイを採用している場合はどちらが現在有効かを確認し、初回は必要に応じて手動でALBのHTTPリスナーのターゲットグループを変更してください。

また、その場合はBlue/Greenデプロイの参照リスナーも変更しておく必要があります。

resource "aws_codedeploy_deployment_group" "example_api" {
  (省略)
  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
-       listener_arns = [aws_lb_listener.example_api_https.arn]
+       listener_arns = [aws_lb_listener.example_api_http.arn]
      }

変更反映後は、実際に本番環境のAPIエンドポイントに向けて疎通確認を行っておくと良いでしょう。

導入後ベンチマーク

導入前と同条件でベンチマークを行います。

結果は以下のようになりました。

Lifting the server siege...
Transactions:                441 hits
Availability:             100.00 %
Elapsed time:              60.80 secs
Data transferred:           0.00 MB
Response time:              0.34 secs
Transaction rate:           7.25 trans/sec
Throughput:             0.00 MB/sec
Concurrency:                2.44
Successful transactions:         441
Failed transactions:               0
Longest transaction:            1.41
Shortest transaction:           0.09

当初の想定に反し、応答速度に有意な変化は見られない結果となりました。

ここはキャッシュの適切な運用を行うことによって改善する可能性があります。

事後作業

CloudFrontを経由しないALBへの直アクセスは不正なリクエストとみなし、弾くための設定を追加しておきます。

以下の公式が提供している手順を踏襲します。

docs.aws.amazon.com

まず、CloudFrontを経由したリクエストに対してカスタムヘッダーを付与するように設定します。

カスタムヘッダーの値は機密性を担保する必要があるため、ランダムな文字列を生成してterraformの外部環境変数として保持するようにしました。

カスタムヘッダー名は任意の判別可能な名前としています。

+ variable "custom_header_value" {
+   type      = string
+   sensitive = true
+ }
+
  resource "aws_cloudfront_distribution" "example_cloudfront" {
    origin {
      domain_name = aws_lb.example_api.dns_name
      origin_id   = aws_lb.example_api.dns_name
      custom_origin_config {
        http_port              = 80
        https_port             = 443
        origin_protocol_policy = "http-only"
        origin_ssl_protocols   = ["TLSv1.2"]
      }
+
+     custom_header {
+       name  = <custom-header-name>
+       value = var.custom_header_value
+     }

次に、上記のカスタムヘッダーが付与されていないリクエストについて、403の固定レスポンスを返すようにALBのリスナールールを設定します。

  default_action {
-   target_group_arn = aws_lb_target_group.example_api_blue.arn
-   type             = "forward"
- }
+   type = "fixed-response"
- lifecycle {
-   ignore_changes = [
-     default_action
-   ]
+   fixed_response {
+     content_type = "text/plain"
+     message_body = "Access denied"
+     status_code  = "403"
+   }
  }

(省略)

+ resource "aws_lb_listener_rule" "example_api" {
+   listener_arn = aws_lb_listener.example_api_http.arn
+   priority     = 10
+
+   action {
+     type             = "forward"
+     target_group_arn = aws_lb_target_group.example_api_blue.arn
+   }
+
+   condition {
+     http_header {
+       http_header_name = <custome-header-name>
+       values           = [var.custom_header_value]
+     }
+   }
+
+   tags = {
+     "Name" = "example-api"
+   }
+   tags_all = {
+     "Name" = "example-api"
+   }
+
+   lifecycle {
+     ignore_changes = [
+       action
+     ]
+   }
+ }

ここではもともとデフォルトルールに転送設定を書いていたため、新しいリスナールールを追加してそちらに移行しました。

まとめ

今回の記事では、AWSのRoute53 + ALB環境にCloudFrontを導入する手順について解説しました。

個人的に、Terraformを利用した管理も含めて弊サービスのインフラ周りを初めてちゃんと触る機会となり、学びが多かったです。

その後セキュリティ向上のためにWAFの導入も行ったため、次回記事ではそちらについてお話しできればと思います。

今年から始まった弊社のテックブログ、2025年も是非よろしくお願いいたします。

それではみなさま良いお年を!


テテマーチでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! herp.careers

エンジニアチームガイドはこちら! tetemarche01.notion.site