はじめに
こんにちは!Product Unit SINIS for X 開発チームの西野(@fingerEase24)です。
今回はAWSのRoute53 + Application Load Balancer(ALB)環境で構築されたAPIサーバーにCloudFrontを導入する作業を行うにあたっての流れや躓きポイントについて説明します。
なお、インフラ環境についてはTerraformによるIaC管理を行っています。
イントロダクション
想定読者
- AWSでのインフラ構築経験がある方
- TerraformやCloudFormationなどによるIaC管理を行ったことがある方
なぜCloudFrontの導入に至ったか
きっかけはXの以下の投稿でした。
ALBのみで事足りる場合も、キャッシュポリシー: CachingDisabledでCloudFrontを間に入れれば無料枠1TBで通信コストが削減できて、エッジロケーションからAWSバックボーンの高速ネットワークを通るので通信レイテンシー改善も見込める
— あんどぅ (@integrated1453) 2024年9月2日
メリットしかないので、CDN使わないという選択肢は実質ないですね〜 https://t.co/WtzQ6syFt0
本記事のケースではAPIサーバーというユースケース上、キャッシュは無効化しておりキャッシュ戦略によるCDNの恩恵は受けていませんが、その場合でもCloudFrontの導入によってエッジロケーションからのアクセスによる通信の高速化や、無料枠によるネットワーク転送料削減などのメリットが生じます。
また、オリジンサーバーの前にリバースプロキシとしてCloudFrontを挟むことにより、セキュリティの向上も見込めます。
導入手順
導入にあたって、以下の手順で作業を行いました。
- 導入前ベンチマーク
- バージニア北部でACM証明書を取得(Terraform管理外)
- CloudFrontロギング用S3バケットの設定
- CloudFrontディストリビューションの作成
- ALBにHTTPリスナーを追加
- CloudFront疎通確認
- Athenaからのログ参照設定
- 運用開始(Route53の向き先をALBからCloudFrontに変更)
- 導入後ベンチマーク
- 事後作業
順に解説していきます。
導入前ベンチマーク
直接的に必要な作業ではありませんが、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
)に限定されています。
そのため、ここはTerraform管理外として手動で作成しました。
AWS Certificate Manager(ACM)にバージニア北部リージョンでアクセスし、「証明書をリクエスト」から証明書のリクエストを行います。
証明書タイプは「パブリック証明書をリクエスト」を選択し、次へ進みます。
ドメイン名には現在のAPIサーバーのドメインを入力し、検証方法は「DNS検証」を指定します。
キーアルゴリズムには現在のシステム構成とサービス要件にもよりますが、可能であれば RSA 2048
よりも暗号化強度の高い ECDSA P 256
を選ぶと良いでしょう。
なお、現状CloudFrontは ECDSA P 384
には対応していないようです。
以上でリクエストを行います。
CloudFrontロギング用S3バケットの設定
ここからTerraform管理による構築を行います。
まずはCloudFrontからロギングを行うための準備をします。
CloudFrontからのS3へのロギングを行うためにはバケットのACLを有効にする必要があるのでその設定を追加します。
# バケット 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 } } }
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" } } }
これにより、レイテンシーのモニタリングなどが有効になります。
詳しくは以下をご覧ください。
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を利用してログ解析を行うための準備をしておきます。
ここでは手順の詳細は省きますが、以下の公式記事が参考になります。
運用開始(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への直アクセスは不正なリクエストとみなし、弾くための設定を追加しておきます。
以下の公式が提供している手順を踏襲します。
まず、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