こしあん
2024-12-08

CloudFront+OACによるS3のホームページホスト


6{icon} {views}

Amazon S3の静的サイトホスティング機能を利用すると、ホームページをホストすることは可能ですが、HTTPS非対応やバケット全体の公開などのセキュリティリスクがあります。そこで、CloudFrontのOAC(Origin Access Control)を活用して、S3バケットを非公開にし、CloudFront経由のみで安全にコンテンツを配信する方法を試してみました。

はじめに

  • S3バケットには静的ページのホスト機能がついており、そのままでホームページをホストすることは可能
  • しかし、S3単独だとHTTPSに対応していなかったり、バケット全体を公開してしまったりいろいろセキュリティリスクはある
  • そこで(教科書的なパターンであるが)CloudFrontのOAC(Origin Access Control)を活用して、S3バケットを公開せず、CloudFront経由の接続のみ配信するという手法を試す

参考:Amazon CloudFront オリジンアクセスコントロール(OAC)のご紹介

関連:[Terraform]CloudFront+S3でホームページを作る

アーキテクチャー図

作るホームページ

以下の2ファイルからなるホームページ

  • index.html
  • images/sample_image.webp

index.htmlは以下の通り。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>静的サイトのサンプル</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 50px;
            background-color: #f0f0f0;
        }
        img {
            max-width: 100%;
            height: auto;
        }
        .container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            display: inline-block;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>ようこそ!</h1>
        <p>これはTerraformを使用してホストされた静的サイトです。</p>
        <img src="images/sample_image.webp" alt="サンプル画像">
    </div>
</body>
</html>

images/sample_image.webpは以下の通り。Dalle-3で作ったもの。かわいい

Terraformのコード

そこまで分量ないのでTerraformのコード全体を示す。仕組みはとても単純で、

  • CloudFront側でOACを定義
  • それをS3のバケットポリシーに入れて許可する
  • CloudFrontのその他の設定は適当
# --- S3 バケット作成 ---
resource "aws_s3_bucket" "this" {
  bucket        = "private-static-site-bucket-test"
  force_destroy = true
}

# index.html をS3にアップロード
resource "aws_s3_object" "index_html" {
  bucket       = aws_s3_bucket.this.bucket
  key          = "index.html"
  content_type = "text/html"
  source       = "index.html"
  acl          = "private"

  depends_on = [aws_s3_bucket_policy.this]
}

# 画像ファイルをS3にアップロード
resource "aws_s3_object" "sample_image" {
  bucket       = aws_s3_bucket.this.bucket
  key          = "images/sample_image.webp"
  source       = "images/sample_image.webp"
  content_type = "image/webp"
  acl          = "private"

  depends_on = [aws_s3_bucket_policy.this]
}

# --- Origin Access Control 作成 ---
resource "aws_cloudfront_origin_access_control" "this" {
  name                              = "my-oac"
  description                       = "OAC for S3 bucket"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
  origin_access_control_origin_type = "s3"
}

# CloudFront Distribution
resource "aws_cloudfront_distribution" "this" {
  enabled             = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"

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

  default_cache_behavior {
    target_origin_id       = "s3-origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  wait_for_deployment = true
}

# S3バケットポリシー: CloudFront Distribution(OAC)からのみオブジェクト取得を許可
data "aws_caller_identity" "current" {}

resource "aws_s3_bucket_policy" "this" {
  bucket = aws_s3_bucket.this.id

  depends_on = [aws_cloudfront_distribution.this]

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontServicePrincipal"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.this.arn}/*"
        Condition = {
          StringEquals = {
            "aws:SourceArn" = "arn:aws:cloudfront::${data.aws_caller_identity.current.account_id}:distribution/${aws_cloudfront_distribution.this.id}"
          }
        }
      }
    ]
  })
}

# CloudFrontのドメインを出力
output "cloudfront_domain" {
  value = aws_cloudfront_distribution.this.domain_name
}

ここでのバケットポリシーは明示的な許可を追加しただけなので、マネジメントコンソールからIAMユーザーはファイルにアクセスできる。ただ、S3単独のパブリックアクセスは許可しなくていいよというもの。

結果

CloudFront経由でアクセスした結果

マネジメントコンソールに表示されているS3のオブジェクトURLからアクセスした結果(Access Deniedで正常な挙動)



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です