ローカルWordPressの静的サイトをS3+CloudFrontで公開する

AWS

概要

ローカルのDocker WordPressでコンテンツを管理し、Simply Staticで静的ファイルを生成、S3+CloudFrontでWeb公開する構成を構築した記録です。

AWS部分はAWS謹製Kiroに手伝ってもらって作成しました。

構成

ローカルWordPress(Docker)
↓ Simply Static で静的生成
S3バケット(静的コンテンツ格納)
↓ OAC経由
CloudFront(CDN + HTTPS)
↑ CNAME
www.example.com(既存DNSサーバー)

S3バケットの作成

パブリックアクセスを全遮断し、CloudFrontのOACからのみアクセスを許可する構成。

# s3.tf

resource "aws_s3_bucket" "static_site" {
  bucket = "<バケット名>"
}

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
}

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

data "aws_iam_policy_document" "static_site_bucket_policy" {
  statement {
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.static_site.arn}/*"]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.static_site.arn]
    }
  }
}

resource "aws_s3_bucket_versioning" "static_site" {
  bucket = aws_s3_bucket.static_site.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "static_site" {
  bucket = aws_s3_bucket.static_site.id

  rule {
    id     = "delete-old-versions"
    status = "Enabled"

    filter {}

    noncurrent_version_expiration {
      noncurrent_days = 7
    }

    abort_incomplete_multipart_upload {
      days_after_initiation = 7
    }
  }
}
  • ポイント:
    • S3の静的ウェブサイトホスティング機能は使わない(パブリックアクセス不要)
    • OACを使うことでCloudFrontからのアクセスのみに限定できる
    • バージョニングを有効にしつつ、ライフサイクルで旧バージョンを7日後に削除してコスト抑制

CloudFront Functionの作成

S3はApacheの`DirectoryIndex`相当の機能がないため、`/path/`へのアクセスを`/path/index.html`に書き換えるFunctionを用意する。

// cf_function.js

function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  } else if (!uri.includes('.')) {
    request.uri += '/index.html';
  }

  return request;
}

CloudFrontディストリビューションの作成

# cloudfront.tf

resource "aws_cloudfront_function" "index_rewrite" {
  name    = "index-rewrite"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = file("${path.module}/cf_function.js")
}

resource "aws_cloudfront_origin_access_control" "static_site" {
  name                              = "static-site-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "static_site" {
  enabled             = true
  default_root_object = "index.html"
  comment             = "Static site distribution"

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

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

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.index_rewrite.arn
    }
  }

  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.main.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  aliases = ["www.example.com"]
}

output "cloudfront_domain" {
  value = aws_cloudfront_distribution.static_site.domain_name
}

ACM証明書の発行

CloudFrontに使うSSL証明書は必ずus-east-1で発行する必要がある。

# provider.tf に追加

provider "aws" {
  alias   = "us_east_1"
  region  = "us-east-1"
  profile = "<プロファイル名>"
}
# acm.tf

resource "aws_acm_certificate" "main" {
  provider          = aws.us_east_1
  domain_name       = "www.example.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

output "acm_validation_cname" {
  value       = aws_acm_certificate.main.domain_validation_options
  description = "DNSサーバーに追加するCNAMEレコード"
}

DNS設定(既存DNSサーバー側)

Route53に移行せず既存のDNSサーバーをそのまま使う場合、以下の2つのCNAMEを手動で追加する。

ACM検証用CNAME

`terraform output acm_validation_cname` で確認した値を登録。

タイプ:  CNAME
ホスト名:`_xxxxxxxx.www`
VALUE:  `_xxxxxxxx.acm-validations.aws`

www → CloudFront

タイプ:  CNAME
ホスト名:`www`
VALUE:  `xxxxxx.cloudfront.net`

注意: お名前.comなどレジストラによっては、DNS管理画面が複数あることがある。`dig @<実際のNSサーバー>`で登録先を確認すること。

# 実際のNSサーバーに直接問い合わせて確認
dig CNAME www.example.com @<NSサーバー>

WordPress側の設定

Simply Staticの「Destination URL」を`https://www.example.com`に設定して静的ファイルを再生成。

注意: WordPressのサイトURLをCloudFrontのURLに変更すると管理画面にアクセスできなくなる。`wp-config.php`に以下を追加してlocalhostに固定しておく。

define('WP_HOME', 'https://www.51weblab.jp');
define('WP_SITEURL', 'https://www.51weblab.jp');

この設定があるとDBの値より`define`が優先されるため、管理画面はlocalhostで動作しつつ、Simply StaticのDestination URLだけ本番ドメインに向けることができる。

S3へのアップロード

aws s3 sync ./static-out/ s3://<バケット名>/ --profile <プロファイル名> --delete

まとめ

<登場人物>
- コンテンツ管理 : ローカルDocker WordPress
- 静的生成      : Simply Static
- ホスティング   : S3(パブリックアクセス遮断)
- CDN/HTTPS    : CloudFront + ACM(無料・自動更新)
- DNS          : 既存DNSサーバーにCNAMEのみ追加

- ブログ原案     : Kiro
- ブログ清書     : 51WebLab.
- 構成図        : 51WebLab.

(本当は順番は逆ですが)Geminiに続いて、またしてもほとんどコードを書かずにこんな環境が構築出来てしまいました。AIに使われないように精進しなければ。

ではでは。