本記事の目標
CloudFrontからInternal ALBに直接接続できる「VPC Origin」という機能が2024年11月にGAされました。本記事では、この機能を活用してセキュアなアーキテクチャをTerraformで構築する手順を解説します。
従来、CloudFrontからプライベートサブネット内のALBにアクセスするには、パブリックサブネットにALBを配置するか、AWS PrivateLinkを使用する必要がありました。VPC Originの登場により、Internal ALBをプライベートサブネットに配置したまま、CloudFrontから直接アクセスできるようになりました。
事前準備
変数の定義
以下の変数を定義しておきます。
variable "prefix" {
type = string
description = "リソース名のプレフィックス"
}
variable "aws_region" {
type = string
default = "ap-northeast-1"
}
variable "sub_domain" {
type = string
description = "使用するサブドメイン(例: demo.example.com)"
}
variable "parent_domain" {
type = string
description = "親ドメイン(例: example.com)"
}
variable "parent_domain_cross_account_role_arn" {
type = string
description = "親ドメインが別アカウントにある場合のクロスアカウントロールARN"
}
variable "tags" {
type = map(string)
default = {}
}
プロバイダーの設定
CloudFront用のACM証明書はus-east-1に作成する必要があるため、プロバイダーを2つ定義します。
provider "aws" {
region = var.aws_region
}
provider "aws" {
alias = "virginia"
region = "us-east-1"
}
ACM証明書の作成
CloudFront用とALB用で、それぞれACM証明書を作成します。CloudFrontのACM証明書はus-east-1に作成する必要がある点に注意してください。
CloudFront用ACM証明書(us-east-1)
resource "aws_acm_certificate" "cloudfront" {
provider = aws.virginia
domain_name = "*.${var.sub_domain}"
subject_alternative_names = [var.sub_domain]
validation_method = "DNS"
}
resource "aws_acm_certificate_validation" "cloudfront" {
provider = aws.virginia
certificate_arn = aws_acm_certificate.cloudfront.arn
validation_record_fqdns = [for record in aws_route53_record.cloudfront_validation : record.fqdn]
}
ワイルドカード証明書(*.sub_domain)をメインにしつつ、SAN(Subject Alternative Name)としてApex(sub_domain)も含めています。これにより、a.demo.example.comやb.demo.example.comだけでなく、demo.example.comへのアクセスにも対応できます。
ALB用ACM証明書(ap-northeast-1)
resource "aws_acm_certificate" "alb" {
domain_name = "*.${var.sub_domain}"
subject_alternative_names = [var.sub_domain]
validation_method = "DNS"
}
resource "aws_acm_certificate_validation" "alb" {
certificate_arn = aws_acm_certificate.alb.arn
validation_record_fqdns = [for record in aws_route53_record.alb_validation : record.fqdn]
}
ALB用の証明書は、ALBと同じリージョン(ap-northeast-1)に作成します。
VPCとInternal ALBの作成
VPCの作成
本記事ではVPCモジュールを使用していますが、プライベートサブネットを持つVPCであれば問題ありません。
module "vpc" {
source = "../vpc"
prefix = var.prefix
aws_region = var.aws_region
tags = var.tags
vpc_cidr_block = "10.0.0.0/16"
public_availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
private_availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
need_nat_gateway = false
}
VPC Originを使用する場合、NATゲートウェイは不要です。CloudFrontからの通信はAWSのプライベートネットワーク経由で行われるためです。
Internal ALBの作成
resource "aws_lb" "this" {
name = "${var.prefix}-alb"
load_balancer_type = "application"
internal = true
idle_timeout = 60
subnets = module.vpc.private_subnet_ids
security_groups = [aws_security_group.alb.id]
}
internal = trueを指定することで、Internal ALBを作成します。プライベートサブネットに配置されるため、インターネットから直接アクセスすることはできません。
セキュリティグループの設定
resource "aws_security_group" "alb" {
name = "${var.prefix}-alb-sg"
vpc_id = module.vpc.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "alb_vpc_origin" {
security_group_id = aws_security_group.alb.id
from_port = 443
to_port = 443
ip_protocol = "tcp"
referenced_security_group_id = data.aws_security_group.vpc_origin.id
lifecycle {
create_before_destroy = true
replace_triggered_by = [null_resource.cloudfront_update_trigger]
}
depends_on = [null_resource.cloudfront_update_trigger, data.aws_security_group.vpc_origin]
}
resource "aws_vpc_security_group_egress_rule" "alb" {
security_group_id = aws_security_group.alb.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
ここで重要なのは、CloudFront VPC Originが作成するセキュリティグループ(CloudFront-VPCOrigins-Service-SG)からのインバウンドのみを許可している点です。これにより、CloudFront経由のトラフィックのみがALBにアクセスできます。
VPC Originのセキュリティグループの参照
CloudFront VPC Originを作成すると、AWSが自動的にセキュリティグループを作成します。このセキュリティグループを参照するために、dataソースを使用します。
resource "null_resource" "cloudfront_update_trigger" {
triggers = {
cloudfront_id = aws_cloudfront_distribution.this.id
}
depends_on = [aws_cloudfront_distribution.this, aws_cloudfront_vpc_origin.this]
}
data "aws_security_group" "vpc_origin" {
filter {
name = "group-name"
values = ["CloudFront-VPCOrigins-Service-SG"]
}
filter {
name = "vpc-id"
values = [module.vpc.vpc_id]
}
depends_on = [null_resource.cloudfront_update_trigger]
}
null_resourceを使用しているのは、CloudFront DistributionとVPC Originが作成された後にセキュリティグループが作成されるためです。この依存関係を明示的に設定することで、正しい順序でリソースが作成されます。
ALB ListenerとListener Ruleの設定
HTTPSリスナーの作成
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.this.arn
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.alb.arn
ssl_policy = "ELBSecurityPolicy-2016-08"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "404: Page not found"
status_code = 404
}
}
depends_on = [aws_acm_certificate_validation.alb]
}
デフォルトアクションとして404を返すように設定しています。これにより、定義されていないHostヘッダーでのアクセスはエラーになります。
Listener Ruleの設定
Hostヘッダーに基づいて異なるレスポンスを返すルールを作成します。
resource "aws_lb_listener_rule" "a" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "This is A"
status_code = 200
}
}
condition {
host_header {
values = ["a.${var.sub_domain}"]
}
}
}
resource "aws_lb_listener_rule" "b" {
listener_arn = aws_lb_listener.https.arn
priority = 101
action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "This is B"
status_code = 200
}
}
condition {
host_header {
values = ["b.${var.sub_domain}"]
}
}
}
本記事では動作確認のためfixed-responseを使用していますが、実際の運用ではforwardアクションでターゲットグループにルーティングします。
CloudFrontの設定
VPC Originの作成
resource "aws_cloudfront_vpc_origin" "this" {
vpc_origin_endpoint_config {
name = "${var.prefix}-vpc-origin"
arn = aws_lb.this.arn
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols {
items = ["TLSv1.2"]
quantity = 1
}
}
}
origin_protocol_policyをhttps-onlyに設定することで、CloudFrontからALBへの通信もHTTPSで暗号化されます。
Origin Request PolicyとCache Policyの作成
Hostヘッダーをオリジンに転送するため、カスタムポリシーを作成します。
resource "aws_cloudfront_origin_request_policy" "this" {
name = "${var.prefix}-origin-request-policy"
cookies_config {
cookie_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Host"]
}
}
}
resource "aws_cloudfront_cache_policy" "this" {
name = "${var.prefix}-cache-policy"
parameters_in_cache_key_and_forwarded_to_origin {
enable_accept_encoding_gzip = true
enable_accept_encoding_brotli = true
cookies_config {
cookie_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Host"]
}
}
}
}
Hostヘッダーを転送することが重要です。これにより、ALBのListener Ruleでホストベースルーティングが機能します。Cache PolicyでもHostヘッダーをキャッシュキーに含めることで、異なるホストからのリクエストが適切にキャッシュされます。
CloudFront Distributionの作成
resource "aws_cloudfront_distribution" "this" {
origin {
domain_name = var.sub_domain
origin_id = aws_lb.this.id
vpc_origin_config {
vpc_origin_id = aws_cloudfront_vpc_origin.this.id
}
}
enabled = true
aliases = [var.sub_domain, "*.${var.sub_domain}"]
default_cache_behavior {
allowed_methods = ["HEAD", "GET"]
cached_methods = ["HEAD", "GET"]
target_origin_id = aws_lb.this.id
viewer_protocol_policy = "https-only"
min_ttl = 0
default_ttl = 60
max_ttl = 60
cache_policy_id = aws_cloudfront_cache_policy.this.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.this.id
}
restrictions {
geo_restriction {
locations = ["JP"]
restriction_type = "whitelist"
}
}
viewer_certificate {
cloudfront_default_certificate = false
acm_certificate_arn = aws_acm_certificate.cloudfront.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
}
aliasesでワイルドカードとApexの両方を設定し、geo_restrictionで日本からのアクセスのみを許可しています。
Route53の設定
パブリックHosted Zone
パブリックHosted Zoneでは、CloudFrontを指すレコードを作成します。
module "sub_domain" {
source = "../subdomain-phz/sub-phz"
aws_region = var.aws_region
aws_parent_phz_region = var.aws_region
tags = var.tags
parent_domain = var.parent_domain
sub_domain = var.sub_domain
cross_account_role_arn = var.parent_domain_cross_account_role_arn
}
resource "aws_route53_record" "cloudfront" {
zone_id = module.sub_domain.sub_domain_zone_id
name = var.sub_domain
type = "A"
allow_overwrite = true
alias {
name = aws_cloudfront_distribution.this.domain_name
zone_id = aws_cloudfront_distribution.this.hosted_zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "wildcard_cloudfront" {
zone_id = module.sub_domain.sub_domain_zone_id
name = "*.${var.sub_domain}"
type = "A"
allow_overwrite = true
alias {
name = aws_cloudfront_distribution.this.domain_name
zone_id = aws_cloudfront_distribution.this.hosted_zone_id
evaluate_target_health = true
}
}
ACM証明書のDNS検証用レコード
resource "aws_route53_record" "cloudfront_validation" {
for_each = {
for dvo in aws_acm_certificate.cloudfront.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = module.sub_domain.sub_domain_zone_id
allow_overwrite = true
name = each.value.name
records = [each.value.record]
type = each.value.type
ttl = 60
}
プライベートHosted Zone
VPC内からの名前解決用に、プライベートHosted Zoneを作成します。これはVPC Origin経由でCloudFrontからALBにアクセスする際に使用されます。
resource "aws_route53_zone" "private" {
name = var.sub_domain
vpc {
vpc_id = module.vpc.vpc_id
}
}
resource "aws_route53_record" "alb" {
zone_id = aws_route53_zone.private.zone_id
name = var.sub_domain
type = "A"
allow_overwrite = true
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "wildcard_alb" {
zone_id = aws_route53_zone.private.zone_id
name = "*.${var.sub_domain}"
type = "A"
allow_overwrite = true
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "alb_validation" {
for_each = {
for dvo in aws_acm_certificate.alb.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = aws_route53_zone.private.zone_id
allow_overwrite = true
name = each.value.name
records = [each.value.record]
type = each.value.type
ttl = 60
}
デプロイと動作確認
デプロイ
terraform init
terraform plan
terraform apply
初回デプロイ時は、ACM証明書の検証に数分かかる場合があります。また、CloudFront Distributionのデプロイにも数分かかります。
動作確認
デプロイが完了したら、curlで動作を確認します。
# サブドメインAへのアクセス
$ curl https://a.demo.example.com
This is A
# サブドメインBへのアクセス
$ curl https://b.demo.example.com
This is B
# 存在しないサブドメインへのアクセス
$ curl https://unknown.demo.example.com
404: Page not found
Hostヘッダーに基づいて正しくルーティングされていることが確認できます。
実装時のポイント
1. VPC Originのセキュリティグループの依存関係
CloudFront VPC Originを作成すると、AWSが自動的にCloudFront-VPCOrigins-Service-SGというセキュリティグループを作成します。このセキュリティグループはCloudFront Distributionが作成された後に作成されるため、null_resourceとdepends_onを使用して依存関係を明示的に設定する必要があります。
2. Hostヘッダーの転送
VPC Originを使用する場合、CloudFrontのOrigin Request PolicyとCache PolicyでHostヘッダーを転送する設定が必要です。これを忘れると、ALBのListener Ruleでホストベースルーティングが機能しません。
3. ACM証明書のリージョン
CloudFront用のACM証明書は必ずus-east-1に作成する必要があります。一方、ALB用の証明書はALBと同じリージョンに作成します。これはAWSの仕様であり、異なるリージョンの証明書を使用しようとするとエラーになります。
4. NATゲートウェイは不要
VPC Originを使用する場合、CloudFrontからALBへの通信はAWSのプライベートネットワーク経由で行われます。そのため、NATゲートウェイは不要です。これにより、コストを削減できます。
今後の応用
今回はシンプルなfixed-responseを使用しましたが、実際の運用ではECSやEC2のターゲットグループにルーティングすることで、複数のサービスを1つのALBで提供できます。
また、WAFをCloudFrontに設定することで、より高度なセキュリティ対策を実装することも可能です。VPC Originと組み合わせることで、Internal ALBを使用しながらもWAFによる保護が受けられます。これは今後機会があれば検証していきたいです。
まとめ
本記事では、CloudFront VPC Originを使用してInternal ALBにセキュアに接続する構成をTerraformで構築する手順を解説しました。
VPC Originの登場により、これまでパブリックサブネットに配置せざるを得なかったALBを、プライベートサブネットに配置できるようになりました。これにより、セキュリティが向上するだけでなく、構成がシンプルになりコスト削減にもつながります。
特に、セキュリティグループの依存関係やHostヘッダーの転送設定など、実装時に注意すべきポイントを重点的に解説しました。