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_behavior を dynamic ブロックで受け取ると、モジュールを汎用的に使いまわせます。
# モジュール変数
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