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?

【Terraform×AWS】S3とCloudFront(OAC)で静的サイトを安全に配信する

0
Last updated at Posted at 2026-11-09

静的なWebサイトをAWSで公開するとき、S3の「静的ウェブサイトホスティング」を使う方法がよく紹介されます。手軽ですが、バケットを一般公開する必要があり、HTTPSにも対応していません。

この記事では、S3を非公開のままにし、CloudFrontのOAC(オリジンアクセスコントロール)経由でだけ配信する構成をTerraformで作ります。独自ドメインとHTTPSにも対応させます。

S3の静的ウェブサイトホスティングの問題点

  • バケットを公開する必要がある:誰でもS3に直接アクセスできてしまう。CloudFrontを前に置いても、S3側への直接アクセスは防げない
  • HTTPSに対応していない:ウェブサイトエンドポイントはHTTPのみ
  • キャッシュや配信の最適化がない:利用者から遠いリージョンのS3から毎回配信される

CloudFrontとOACを組み合わせると、S3は完全に非公開のまま、CloudFrontからの読み取りだけを許可できます。

全体の構成

  • S3バケット:サイトのファイルを置く。パブリックアクセスはすべてブロックする
  • CloudFront:利用者からのHTTPSのリクエストを受け、S3から取得したファイルを配信・キャッシュする
  • OAC:CloudFrontがS3にアクセスするときに署名を付け、S3側で「このCloudFrontからだけ」許可できるようにする
  • ACM証明書:独自ドメインのHTTPS用の証明書
  • Route 53:独自ドメインをCloudFrontに向ける

プロバイダーの設定

CloudFrontで使うACM証明書は、us-east-1(バージニア北部)リージョンで作る必要があります。そのため、東京リージョンのプロバイダーとは別に、us-east-1用のプロバイダーを用意します。

provider "aws" {
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

variable "domain_name" {
  type        = string
  description = "例:www.example.com"
}

variable "hosted_zone_name" {
  type        = string
  description = "例:example.com"
}

variable "bucket_name" {
  type = string
}

東京リージョンで証明書を作ってしまうと、CloudFrontに設定する段階でエラーになります。初めての人がつまずきやすい点です。

S3バケット

resource "aws_s3_bucket" "site" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_public_access_block" "site" {
  bucket                  = aws_s3_bucket.site.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

パブリックアクセスのブロックをすべて有効にします。静的ウェブサイトホスティングの設定(aws_s3_bucket_website_configuration)は作りません。

ACM証明書とDNS検証

証明書はus-east-1のプロバイダーで作り、Route 53のレコードでドメインの所有を検証します。

data "aws_route53_zone" "this" {
  name = var.hosted_zone_name
}

resource "aws_acm_certificate" "site" {
  provider          = aws.us_east_1
  domain_name       = var.domain_name
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.site.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id = data.aws_route53_zone.this.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 300
}

resource "aws_acm_certificate_validation" "site" {
  provider                = aws.us_east_1
  certificate_arn         = aws_acm_certificate.site.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

aws_acm_certificate_validationは、検証が完了するまでapplyを待ってくれるリソースです。CloudFrontはこのリソースの証明書ARNを参照することで、検証済みの証明書ができてから作られる順序になります。

OACとCloudFrontのディストリビューション

resource "aws_cloudfront_origin_access_control" "site" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

data "aws_cloudfront_cache_policy" "optimized" {
  name = "Managed-CachingOptimized"
}

resource "aws_cloudfront_distribution" "site" {
  enabled             = true
  default_root_object = "index.html"
  aliases             = [var.domain_name]

  origin {
    domain_name              = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id                = "s3-site"
    origin_access_control_id = aws_cloudfront_origin_access_control.site.id
  }

  default_cache_behavior {
    target_origin_id       = "s3-site"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    cache_policy_id        = data.aws_cloudfront_cache_policy.optimized.id
    compress               = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate_validation.site.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}

ポイントは次のとおりです。

  • origin.domain_name:bucket_regional_domain_name(S3のREST APIのエンドポイント)を使う。ウェブサイトエンドポイントではOACが使えない
  • signing_behavior = "always":CloudFrontからS3へのリクエストに、常に署名を付ける
  • viewer_protocol_policy = "redirect-to-https":HTTPでアクセスされたら、HTTPSにリダイレクトする
  • cache_policy_id:AWSが用意しているキャッシュポリシー(Managed-CachingOptimized)を使う。独自に細かく設定するより、まずはこれで十分

S3のバケットポリシー

最後に、S3側で「このCloudFrontディストリビューションからの読み取りだけ」を許可します。

data "aws_iam_policy_document" "site" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.site.arn}/*"]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.site.arn]
    }
  }
}

resource "aws_s3_bucket_policy" "site" {
  bucket = aws_s3_bucket.site.id
  policy = data.aws_iam_policy_document.site.json
}

Principalは「CloudFrontサービス全体」ですが、conditionでAWS:SourceArnを自分のディストリビューションに限定しています。この条件がないと、他人のCloudFrontからでもバケットを読めてしまいます。

独自ドメインをCloudFrontに向ける

resource "aws_route53_record" "site" {
  zone_id = data.aws_route53_zone.this.zone_id
  name    = var.domain_name
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.site.domain_name
    zone_id                = aws_cloudfront_distribution.site.hosted_zone_id
    evaluate_target_health = false
  }
}

CloudFrontには固定のIPアドレスがないため、CNAMEではなくRoute 53のエイリアスレコードを使います。

つまずきやすい点:サブディレクトリのindex.html

default_root_objectが効くのは、ドメイン直下(/)へのアクセスだけです。/about/ のようなサブディレクトリにアクセスしても、about/index.html は自動では返されず、エラーになります。

これを解決するには、CloudFront Functionsでリクエストのパスの末尾に index.html を付け足すか、ファイルの置き方を「about.html」のようにURLと一致させます。Next.jsなどの静的書き出しを使う場合は、出力の形式を確認しておきましょう。

ファイルのデプロイ

ファイルをS3に同期したあと、CloudFrontのキャッシュを削除します。

aws s3 sync ./dist s3://my-site-bucket --delete
aws cloudfront create-invalidation \
  --distribution-id E1234567890ABC \
  --paths "/*"

キャッシュの削除(Invalidation)は、月1,000パスまでは無料です。「/*」は1パスとして数えられるので、デプロイのたびに全体を削除しても、通常は料金がかかりません。

まとめ

  • S3の静的ウェブサイトホスティングではなく、非公開のS3とCloudFront+OACで配信する
  • CloudFront用のACM証明書はus-east-1で作る
  • バケットポリシーはAWS:SourceArnで自分のディストリビューションに限定する
  • サブディレクトリのindex.htmlは自動では返らないので、CloudFront Functionsなどで対応する

S3を一切公開しない構成にしておけば、「バケットの公開設定を間違えて情報が漏れた」という事故そのものが起こらなくなります。

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?