🌐

TerraformでS3 + CloudFrontをモジュール化する

に公開

はじめに

S3 + CloudFrontで静的アセットを配信する構成をTerraformで書いていくと、いくつかハマりどころがあります。この記事ではその対処法をまとめます。

ユーザー → CloudFront (assets.example.com) → S3バケット (パブリックアクセス無効)

CloudFront用ACMはus-east-1固定

CloudFrontにカスタムドメインを設定するにはACM証明書が必要ですが、us-east-1(バージニア北部)に作成した証明書しか使えません

他のリソースをap-northeast-1で管理していても、CloudFront用の証明書だけはus-east-1に作る必要があります。


provider alias を使う

provider "aws"alias を付けることで、同じプロバイダーを複数リージョンで使い分けられます。

# デフォルトプロバイダー (ap-northeast-1)
provider "aws" {
  region = "ap-northeast-1"
}

# us-east-1エイリアス (CloudFront ACM用)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

既存のACMモジュールを呼び出すとき、providers パラメータで渡すだけで再利用できます。

# ALB用証明書 (ap-northeast-1)
module "acm" {
  source         = "../../modules/aws/acm"
  domain_name    = var.domain_name
  hosted_zone_id = module.dns.hosted_zone_id
  # ...
}

# CloudFront用証明書 (us-east-1 固定)
module "cdn_acm" {
  source = "../../modules/aws/acm"

  providers = {
    aws = aws.us_east_1  # ← これだけ
  }

  domain_name    = "assets.${var.domain_name}"
  hosted_zone_id = module.dns.hosted_zone_id
  # ...
}

モジュール側は何も変えずに、Route53のホストゾーンはグローバルリソースなので同じ hosted_zone_id を使いまわせます。


OACでS3への直接アクセスを禁止する

S3をオリジンにするとき、「CloudFrontだけがS3にアクセスできる」ように制限することが推奨されています。S3バケットURLへの直接アクセスを禁止することで、CloudFrontのアクセス制御(署名付きURLや地理的制限など)をバイパスされるリスクを防げます。従来はOAI(Origin Access Identity)が使われていましたが、現在はOAC(Origin Access Control)が推奨です。

項目 OAI OAC
新規構築での推奨 ×
バケットポリシーの条件 OAIのARN AWS:SourceArnでdistributionのARNを指定

OACの設定はこうなります。

resource "aws_cloudfront_origin_access_control" "main" {
  name                              = "my-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "main" {
  origin {
    domain_name              = aws_s3_bucket.assets.bucket_regional_domain_name
    origin_id                = "s3-origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.main.id
  }

  viewer_certificate {
    acm_certificate_arn      = module.cdn_acm.certificate_arn  # us-east-1のARN
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
  # ...
}

バケットポリシー側は cloudfront.amazonaws.com サービスプリンシパルを使い、AWS:SourceArn でdistributionを限定します。

{
  Sid       = "AllowCloudFrontOAC"
  Effect    = "Allow"
  Principal = { Service = "cloudfront.amazonaws.com" }
  Action    = "s3:GetObject"
  Resource  = "${aws_s3_bucket.assets.arn}/*"
  Condition = {
    StringEquals = {
      "AWS:SourceArn" = aws_cloudfront_distribution.main.arn
    }
  }
}

パスパターン別TTLを動的に設定する

画像(変更頻度低)はTTL 30日、JSONなど(更新されうる)はTTL 1時間、というように分けたい場合があります。ordered_cache_behaviordynamic ブロックで受け取ると、モジュールを汎用的に使いまわせます。

# モジュール変数
variable "ordered_cache_behaviors" {
  type = list(object({
    path_pattern = string
    default_ttl  = number
    max_ttl      = number
  }))
  default = []
}
# モジュール内
dynamic "ordered_cache_behavior" {
  for_each = var.ordered_cache_behaviors
  content {
    path_pattern = ordered_cache_behavior.value.path_pattern
    default_ttl  = ordered_cache_behavior.value.default_ttl
    max_ttl      = ordered_cache_behavior.value.max_ttl
    # ...
  }
}

呼び出し側はこう書くだけです。

module "cloudfront_assets" {
  source              = "../../modules/aws/cloudfront"
  acm_certificate_arn = module.cdn_acm.certificate_arn

  ordered_cache_behaviors = [
    {
      path_pattern = "*.json"
      default_ttl  = 3600   # 1時間
      max_ttl      = 86400
    }
  ]
}

まとめ

ポイント 対応
CloudFront用ACMはus-east-1固定 provider "aws" { alias = "us_east_1" } を追加
既存ACMモジュールの再利用 providers = { aws = aws.us_east_1 } を指定するだけ
S3への直接アクセス禁止 OAC + AWS:SourceArn でdistribution限定
パスパターン別TTL dynamic "ordered_cache_behavior" で可変に

provider alias は地味な機能ですが、「us-east-1にしかないリソース」を扱うときに必須の知識です。CloudFront + ACMは典型的なユースケースなので、一度モジュール化しておくと使いまわせます。


参考リンク

Discussion