TerraformでCloudFrontとS3の静的サイトをコード管理する方法|OAC・HTTPSリダイレクト・キャッシュポリシーの実践設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerraformでCloudFrontとS3の静的サイトをコード管理する方法|OAC・HTTPSリダイレクト・キャッシュポリシーの実践設計
「S3バケットにウェブサイトをホスティングしているが、毎回コンソールからCloudFrontを手動設定している」「本番・検証・開発の3環境に同じ設定を繰り返す度に、細かい差異でバグが出る」
CloudFrontとS3を組み合わせた静的サイト配信は、設定項目が多く手動管理では属人化しやすい構成です。Terraformでコード管理すれば、設定の意図がHCLに記述されてGitで変更履歴を追跡でき、複数環境への展開もモジュール化で効率化できます。

この記事では、TerraformでCloudFront+S3の静的サイト配信基盤をゼロから構築する方法を解説します。旧方式のOAI(Origin Access Identity)ではなく現在推奨のOAC(Origin Access Control)でS3を保護する設計、HTTPSリダイレクト、マネージドキャッシュポリシー、ACM証明書とRoute 53レコードの管理まで、実際のHCLコードと実行ログで説明します。

動作確認環境: Terraform 1.7.x、hashicorp/aws プロバイダー 5.x、RHEL 9.4

この記事のポイント

・OAI(旧)ではなくOAC(Origin Access Control)でS3を保護するのが現在の推奨設計
・S3バケットはパブリックアクセスを全ブロックし、OACからのGetObjectのみをバケットポリシーで許可する
・CloudFrontのviewer_protocol_policy = "redirect-to-https"でHTTPSへの強制リダイレクトを設定できる
・ACM証明書はCloudFrontのグローバル要件によりus-east-1リージョンで作成する必要がある


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜCloudFront+S3の構成をTerraformで管理するのか

S3とCloudFrontを使った静的サイト配信は、コスト・スケーラビリティ・グローバルキャッシュの観点で優れた構成です。一方でコンソールから手動設定する場合、以下の問題が発生しやすいです。

設定の属人化:「なぜこのキャッシュポリシーを選んだのか」「OACのsigning_behaviorは何にすべきか」といった判断の根拠がドキュメントに残らない
環境差異によるバグ:本番・検証・開発の各環境を手作業で構築すると、微妙な設定差がキャッシュ動作の違いやHTTPS設定漏れの原因になる
セキュリティ設定の抜け漏れ:OACの設定が不完全でS3バケットが意図せず公開されたり、HTTPSリダイレクトが未設定のまま本番稼働したりするリスクがある

Terraformでコード管理すれば、設定の意図がHCLに記述され、変更差分はGitで追跡でき、複数環境への展開もモジュール化で効率化できます。また terraform plan で「次のapplyで何が変わるか」を実行前に確認できるため、意図しない設定変更のリスクも下がります。

全体構成と必要なAWSリソース

この記事で構築する構成を確認しておきます。ユーザーのリクエストはCloudFrontが受け付け、S3バケット内の静的ファイルをオリジンとして配信します。S3バケットはパブリックアクセスをすべてブロックし、CloudFrontからのアクセスのみをOACで許可します。

Terraformで管理するリソースは以下の7種類です。

aws_s3_bucket — 静的ファイルを格納するS3バケット
aws_s3_bucket_public_access_block — パブリックアクセスの全ブロック
aws_cloudfront_origin_access_control — OAC(CloudFrontとS3の認証済みアクセス)
aws_s3_bucket_policy — OACからのGetObjectのみを許可するバケットポリシー
aws_cloudfront_distribution — CloudFront配信設定(キャッシュ・HTTPS・オリジン)
aws_acm_certificate / aws_acm_certificate_validation — SSL/TLS証明書(us-east-1必須)
aws_route53_record — CloudFrontへのエイリアスAレコード

今回のコードは s3.tfcloudfront.tfacm.tfroute53.tfvariables.tf の5ファイルに分割して管理します。

S3バケットとOACをHCLで定義する

1. S3バケットのパブリックアクセスをブロックする

まずS3バケットを作成し、パブリックアクセスを4項目すべてブロックします。OAC経由のCloudFrontアクセスのみを後続のバケットポリシーで許可します。

# s3.tf resource "aws_s3_bucket" "static_site" { bucket = "${var.project}-${var.environment}-static-site" tags = { Name = "${var.project}-static-site" Environment = var.environment } } resource "aws_s3_bucket_public_access_block" "static_site" { bucket = aws_s3_bucket.static_site.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }

バケット名はグローバルで一意である必要があります。${var.project}-${var.environment} のような命名規則を採用すると、複数環境を同じコードで管理しやすくなります。

2. OACを作成してバケットポリシーでCloudFrontからのアクセスを許可する

OAC(Origin Access Control)はOAI(Origin Access Identity)の後継です。AWSは新規構築にはOACを推奨しており、OAIは非推奨になっています。OACの主な優位点は、バケットポリシーのConditionで「特定のCloudFrontディストリビューションからのアクセスのみ」を指定できる点です。

# cloudfront_oac.tf(cloudfront.tfと同じファイルでも可) resource "aws_cloudfront_origin_access_control" "static_site" { name = "${var.project}-${var.environment}-oac" description = "OAC for ${var.project} static site S3 origin" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" } # バケットポリシー:CloudFrontのサービスプリンシパルからのGetObjectのみ許可 resource "aws_s3_bucket_policy" "static_site" { bucket = aws_s3_bucket.static_site.id policy = data.aws_iam_policy_document.static_site_policy.json # パブリックアクセスブロックを先に適用してからポリシーを設定する depends_on = [aws_s3_bucket_public_access_block.static_site] } data "aws_iam_policy_document" "static_site_policy" { statement { actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.static_site.arn}/*"] principals { type = "Service" identifiers = ["cloudfront.amazonaws.com"] } condition { test = "StringEquals" variable = "AWS:SourceArn" values = [aws_cloudfront_distribution.static_site.arn] } } }

バケットポリシーの conditionAWS:SourceArn に特定のCloudFront DistributionのARNを指定することで、別のCloudFrontディストリビューションからのアクセスは拒否されます。OAIでは実現できなかったこの粒度の制御がOACの利点です。

depends_on でパブリックアクセスブロック適用を待ってからバケットポリシーを設定する点も重要です。ブロック設定が未完のままポリシーを設定しようとすると、AccessDenied エラーが発生する場合があります。

CloudFront DistributionをHCLで設定する

1. aws_cloudfront_distributionの基本構造

CloudFront Distributionは設定項目が多いリソースです。必須要素を整理して定義します。

# cloudfront.tf resource "aws_cloudfront_distribution" "static_site" { enabled = true default_root_object = "index.html" aliases = [var.domain_name] # 例: "www.example.com" price_class = "PriceClass_200" # 北米・欧州・アジア(東南アジア含む) origin { domain_name = aws_s3_bucket.static_site.bucket_regional_domain_name origin_id = "S3-${aws_s3_bucket.static_site.id}" origin_access_control_id = aws_cloudfront_origin_access_control.static_site.id } default_cache_behavior { target_origin_id = "S3-${aws_s3_bucket.static_site.id}" viewer_protocol_policy = "redirect-to-https" # HTTPをHTTPSへ強制リダイレクト allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] # AWSマネージドキャッシュポリシー(CachingOptimized)を使用 cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id } # SPAのルーティング対応(403/404をindex.htmlへ) custom_error_response { error_code = 403 response_code = 200 response_page_path = "/index.html" } custom_error_response { error_code = 404 response_code = 200 response_page_path = "/index.html" } viewer_certificate { acm_certificate_arn = aws_acm_certificate_validation.static_site.certificate_arn ssl_support_method = "sni-only" minimum_protocol_version = "TLSv1.2_2021" } restrictions { geo_restriction { restriction_type = "none" } } tags = { Name = "${var.project}-cdn" Environment = var.environment } } # AWSマネージドキャッシュポリシーの参照 data "aws_cloudfront_cache_policy" "caching_optimized" { name = "Managed-CachingOptimized" }

2. price_classとviewer_protocol_policyの選び方

price_class はCloudFrontのエッジロケーション利用範囲を制限してコストを抑えるオプションです。

PriceClass_100:北米・欧州のみ(最低コスト)
PriceClass_200:北米・欧州・アジア・中東・アフリカ(推奨バランス)
PriceClass_All:全エッジロケーション(最高コスト・最低レイテンシ)

日本向けサービスでは PriceClass_200 を選択することで、東京・大阪・シンガポールのエッジを利用しながらコストを抑制できます。

viewer_protocol_policy には "redirect-to-https" を必ず設定してください。"allow-all" のままにすると、HTTPでコンテンツが配信され通信の盗聴リスクが生じます。minimum_protocol_version = "TLSv1.2_2021" と組み合わせることで、TLS 1.0/1.1の古いプロトコルも排除できます。

マネージドキャッシュポリシー Managed-CachingOptimized は、S3静的サイトに適したキャッシュTTL設定(デフォルト86400秒・最大31536000秒)とGzip/Brotli圧縮が組み込まれています。独自のキャッシュルールが不要な場合はこのポリシーを使うのが最も簡単です。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformでのCloudFront・S3設計から環境別モジュール構成・CI/CDパイプライン統合まで、本番で使えるIaC設計パターンをハンズオン形式で学べるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

ACM証明書とRoute 53レコードをコード管理する

1. ACM証明書をus-east-1で作成する

CloudFrontに設定するACM証明書は、必ずus-east-1リージョンで作成する必要があります。これはCloudFrontがグローバルサービスであり、エッジで証明書を参照する際にus-east-1を使用するAWSの仕様です。

注意:ap-northeast-1(東京)など別リージョンで作成した証明書はCloudFrontのviewer_certificate設定時にInvalidViewerCertificateエラーになります。既存証明書がus-east-1以外にある場合は、provider alias(aws.us_east_1)を追加してからTerraformで再作成してください。

メインのAWSプロバイダーが別リージョン(例: ap-northeast-1)の場合は、provider alias でus-east-1を追加します。

# providers.tf provider "aws" { region = "ap-northeast-1" # メインリージョン(S3・Route 53等) } provider "aws" { alias = "us_east_1" region = "us-east-1" # ACM証明書専用 } # acm.tf resource "aws_acm_certificate" "static_site" { provider = aws.us_east_1 # us-east-1で作成(必須) domain_name = var.domain_name validation_method = "DNS" lifecycle { create_before_destroy = true } } # DNS検証レコードをRoute 53に自動作成(for_eachでドメイン数に対応) resource "aws_route53_record" "cert_validation" { for_each = { for dvo in aws_acm_certificate.static_site.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } zone_id = data.aws_route53_zone.static_site.zone_id name = each.value.name type = each.value.type records = [each.value.record] ttl = 60 } resource "aws_acm_certificate_validation" "static_site" { provider = aws.us_east_1 certificate_arn = aws_acm_certificate.static_site.arn validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] }

lifecycle { create_before_destroy = true } はACM証明書の更新時に、旧証明書を削除する前に新証明書を先に作成させるための設定です。CloudFrontへの紐付けが残った状態で旧証明書が先に削除されるとダウンタイムが発生するため、証明書リソースには必ず設定してください。

for_each によるDNS検証レコードの自動作成は、ワイルドカード証明書(*.example.com)を追加した場合でも、複数の検証レコードを個別に管理できる実用的なパターンです。

2. Route 53 AレコードでCloudFrontにトラフィックを向ける

CloudFrontのエイリアスレコードはAレコード(aliasタイプ)で設定します。CNAMEではルートドメイン(example.com)に使えません。

# route53.tf data "aws_route53_zone" "static_site" { name = var.root_domain # 例: "example.com" private_zone = false } resource "aws_route53_record" "static_site" { zone_id = data.aws_route53_zone.static_site.zone_id name = var.domain_name # 例: "www.example.com" type = "A" alias { name = aws_cloudfront_distribution.static_site.domain_name zone_id = aws_cloudfront_distribution.static_site.hosted_zone_id evaluate_target_health = false # CloudFrontはヘルスチェック不要 } }

evaluate_target_health = false はCloudFrontのエイリアスレコードでは必ずfalseにします。CloudFrontはAWSのマネージドサービスであり、Route 53のヘルスチェックとは連携していません。

Route 53のDNS設定の詳細についてはLinux DNS設定の基本(resolv.conf・dig・nmcli)も参考にしてください。

terraform applyでデプロイして動作確認する

1. planで変更内容を確認する

HCLコードが揃ったら terraform plan で変更内容を確認します。初回は8リソース前後が + create として表示されます。

$ terraform plan Terraform will perform the following actions: # aws_acm_certificate.static_site will be created + resource "aws_acm_certificate" "static_site" { + id = (known after apply) + domain_name = "www.example.com" + validation_method = "DNS" ... } # aws_cloudfront_distribution.static_site will be created + resource "aws_cloudfront_distribution" "static_site" { + id = (known after apply) + domain_name = (known after apply) + aliases = ["www.example.com"] + price_class = "PriceClass_200" ... } # aws_cloudfront_origin_access_control.static_site will be created + resource "aws_cloudfront_origin_access_control" "static_site" { + signing_behavior = "always" + signing_protocol = "sigv4" ... } # aws_s3_bucket.static_site will be created + resource "aws_s3_bucket" "static_site" { + id = (known after apply) + bucket = "myproject-prod-static-site" ... } Plan: 9 to add, 0 to change, 0 to destroy.

2. applyを実行してリソースを作成する

terraform apply を実行します。ACM証明書のDNS検証が完了するまで5~10分、CloudFront Distributionのデプロイはさらに10~15分かかることがあります。

$ terraform apply -auto-approve aws_s3_bucket.static_site: Creating... aws_s3_bucket.static_site: Creation complete after 3s [id=myproject-prod-static-site] aws_s3_bucket_public_access_block.static_site: Creating... aws_s3_bucket_public_access_block.static_site: Creation complete after 1s aws_cloudfront_origin_access_control.static_site: Creating... aws_cloudfront_origin_access_control.static_site: Creation complete after 2s aws_acm_certificate.static_site: Creating... aws_acm_certificate.static_site: Creation complete after 5s [id=arn:aws:acm:us-east-1:123456789012:certificate/xxxx-xxxx-xxxx-xxxx] # DNS検証レコードを自動作成(約30秒) aws_route53_record.cert_validation["www.example.com"]: Creating... aws_route53_record.cert_validation["www.example.com"]: Creation complete after 32s # ACM証明書のDNS検証完了待ち(5~10分) aws_acm_certificate_validation.static_site: Still creating... [1m0s elapsed] aws_acm_certificate_validation.static_site: Still creating... [4m0s elapsed] aws_acm_certificate_validation.static_site: Creation complete after 5m48s # CloudFront Distributionのデプロイ(10~15分) aws_cloudfront_distribution.static_site: Still creating... [5m0s elapsed] aws_cloudfront_distribution.static_site: Still creating... [10m0s elapsed] aws_cloudfront_distribution.static_site: Creation complete after 13m21s [id=E1XXXXXXXXXXXXXXXXX] Apply complete! Resources: 9 added, 0 changed, 0 destroyed. Outputs: cloudfront_domain_name = "dxxxxxxxxxxxx.cloudfront.net" s3_bucket_name = "myproject-prod-static-site"

デプロイ完了後、S3バケットに index.html をアップロードして動作を確認します。

# テスト用のindex.htmlをS3にアップロード $ aws s3 cp index.html s3://myproject-prod-static-site/ # HTTPからHTTPSへのリダイレクトを確認(301 Moved Permanently) $ curl -I http://www.example.com/ HTTP/1.1 301 Moved Permanently Location: https://www.example.com/ Server: CloudFront X-Cache: Redirect from cloudfront # HTTPSでのアクセスを確認 $ curl -I https://www.example.com/ HTTP/2 200 content-type: text/html x-cache: Miss from cloudfront via: 1.1 xxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront) x-amz-cf-pop: NRT51-P3

x-cache: Miss from cloudfront は初回アクセスでキャッシュがない状態を示します。2回目以降は Hit from cloudfront に変わります。HTTPSポートの疎通確認コマンドについてはLinuxのポート確認コマンド(ss・lsof)も参考にしてください。

トラブルシュート|よくあるエラーと対処

1. ACM証明書のDNS検証が終わらない

aws_acm_certificate_validation が長時間 Still creating... のまま止まる場合、Route 53にDNS検証用のCNAMEレコードが正しく作成されていないことが原因です。

# 証明書の検証ステータスを確認 $ aws acm describe-certificate \ --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxx \ --region us-east-1 \ --query 'Certificate.DomainValidationOptions[0].ValidationStatus' "PENDING_VALIDATION" # Route 53にCNAMEレコードが存在するか確認 $ aws route53 list-resource-record-sets \ --hosted-zone-id /hostedzone/XXXXXXXXXX \ --query 'ResourceRecordSets[?Type==`CNAME`].Name'

CNAMEレコードが存在しない場合は terraform state listaws_route53_record.cert_validation が作成済みか確認し、未作成であれば terraform apply を再実行してください。

2. CloudFront経由でS3から403 Forbiddenが返される

OACのバケットポリシー設定に問題がある場合に発生します。以下の2点を確認してください。

・バケットポリシーの AWS:SourceArn に正しいCloudFront DistributionのARNが設定されているか
・OACの signing_behavior = "always"signing_protocol = "sigv4" が両方設定されているか

また、aws_s3_bucket_public_access_blockaws_s3_bucket_policy の適用順序が問題になる場合があります。depends_on = [aws_s3_bucket_public_access_block.static_site] が設定されているか確認してください。S3バケットポリシーのデバッグには aws s3api get-bucket-policy --bucket バケット名 で現在の適用内容を確認します。

3. カスタムドメインでERR_SSL_PROTOCOL_ERRORが発生する

CloudFront DistributionのSSL証明書設定に問題があります。最も多い原因はACM証明書が us-east-1 以外のリージョンで作成されていることです。

# ACM証明書のリージョンを確認(us-east-1である必要がある) $ terraform state show aws_acm_certificate.static_site | grep -E 'arn|region' id = "arn:aws:acm:us-east-1:123456789012:certificate/xxxx" # ap-northeast-1で誤作成した場合は、provider = aws.us_east_1 を追加して再作成する # terraform destroy -target=aws_acm_certificate.static_site # terraform apply -target=aws_acm_certificate.static_site

本記事のまとめ

設定項目 Terraformリソース / 設定値
S3バケットを非公開にする aws_s3_bucket_public_access_block(4項目true)
CloudFrontのみS3アクセスを許可 aws_cloudfront_origin_access_controlaws_s3_bucket_policy
HTTPSへの強制リダイレクト viewer_protocol_policy = "redirect-to-https"
キャッシュ設定の最適化 data.aws_cloudfront_cache_policy "Managed-CachingOptimized"
SSL証明書の設定 aws_acm_certificate(us-east-1必須)
ACM証明書のDNS検証自動化 aws_route53_record.cert_validation(for_each)+aws_acm_certificate_validation
カスタムドメインのDNS設定 aws_route53_record(aliasブロック、evaluate_target_health=false)
Terraformのlifecycleブロックで本番リソースを誤削除から守る方法|prevent_destroyとignore_changesの実践設計
Terraformのprovider設定とバージョン管理|required_providersとterraform.lock.hclでチーム開発を安定させる方法
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformでのCloudFront+S3配信設計やCI/CDパイプライン統合を含む、本番を壊さないIaC設計スキルをハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。