0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【アドベントカレンダーDay 6】CloudFront VPC Originを使ってInternal ALBにセキュアに接続する構成をTerraformで構築する

Posted at

本記事の目標

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.comb.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_policyhttps-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_resourcedepends_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ヘッダーの転送設定など、実装時に注意すべきポイントを重点的に解説しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?