メインコンテンツへスキップ

【Hugo×AWS】Hugo+S3+CloudFrontで技術ブログを公開する

·
学習・作業ログ AWS Terraform Hugo
目次
Hugo-S3-CloudFrontで技術ブログを公開する - この記事は連載の一部です
パート 1: この記事

はじめに
#

この記事では、Hugoで作成した静的サイトをAWS(S3 + CloudFront)でホスティングする方法を解説します。 Terraformを使ってインフラをコード化し、手動デプロイまでを行います。

想定する読者
#

  • AWSを使って静的サイトをホスティングしたい方
  • Terraformでインフラをコード化したい方
  • AWS初心者〜中級者

Terraformを初めて使う方へ
#

本記事ではTerraformの基本的な使い方(terraform init/plan/applyの意味、HCL構文等)は省略しています。初めての方はHashiCorp公式チュートリアルを先に確認することをお勧めします。

完成イメージ
#

S3+CloudFront構成図

  • 本記事の完了後、CloudFrontのデフォルトドメイン(https://dxxxxx.cloudfront.net)でブログにアクセスできるようになります。
  • CloudFrontからS3へのアクセスにはOAC(Origin Access Control)を使用し、S3バケットへの直接アクセスを禁止します。

シリーズ全体像
#

【Hugo×AWS】シリーズ全体で5記事投稿予定です。今回の記事は1本目です。

#タイトル内容
1Hugo + S3 + CloudFrontで技術ブログを公開するHugo環境構築〜手動デプロイまで
2GitHub Actions + OIDCで自動デプロイCI/CD構築、アクセスキー不要の認証
3CloudWatch + SNSで監視・アラート通知ダッシュボード、エラー率アラーム
4Athenaでアクセスログを分析するCloudFrontログのSQL分析
5独自ドメインを設定する(Route53 + ACM)カスタムドメイン、HTTPS

なぜS3 + CloudFrontで構築するのか
#

静的サイトのホスティング方法として、以下を比較検討しました。

選択肢コスト(月額)管理負担IaC化
S3 + CloudFront100〜170円 ※1なし
EC2 + Nginx1,500円〜OS管理必要
Lightsail500円〜OS管理必要
Amplify Hosting無料枠ありなし×

※1 個人ブログ規模の予想値となります。おおよそのコスト感としてとらえてください。

S3 + CloudFrontを選んだ理由
#

  • コスト効率:静的サイトに最適、月額100円台
  • 運用負担ゼロ:サーバーレスでOS管理不要
  • Terraform対応:全リソースをコードで管理可能
  • 高可用性:S3のSLA 99.99%
  • 高速配信:CloudFrontのエッジキャッシュ
  • セキュリティ:OAC(Origin Access Control)でS3への直接アクセスを禁止

EC2/Lightsailは静的サイトにはオーバースペックで、Amplifyは便利だがTerraformで管理できないため見送りました。

前提条件
#

構築環境
#

本記事は以下の環境で構築しています。

  • OS: Ubuntu 24.04(Docker)
  • Hugo: v0.152.2 extended
  • Terraform: v1.14.2
  • AWS CLI: v2.32.16

必要なツール
#

以下のツールがインストールされていることを確認してください。

ツールバージョン公式ドキュメント
AWS CLIv2インストールガイド
Terraform1.0以上インストールガイド
Git-インストールガイド
Hugo-インストールガイド

AWSアカウントが必要です。ルートユーザーではなく、IAMユーザーでの操作を推奨します。

:::details 参考用:自分が実際に実行したコマンド

AWS CLI
#

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

Terraform
#

sudo apt-get update && sudo apt-get install -y gnupg lsb-release
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform

Hugo
#

# 最新バージョンを確認
curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep "tag_name"

# extended版をダウンロード・インストール
wget https://github.com/gohugoio/hugo/releases/download/v0.152.2/hugo_extended_0.152.2_linux-amd64.deb
sudo dpkg -i hugo_extended_0.152.2_linux-amd64.deb

Git
#

最初からインストール済みのため対応不要でした。

:::

AWS CLIの認証情報を設定していない場合は、以下のコマンドで設定してください。

aws configure

プロンプトに従って入力します。

AWS Access Key ID [None]: {IAMユーザーのアクセスキーを入力}
AWS Secret Access Key [None]: {IAMユーザーのシークレットアクセスキーを入力}
Default region name [None]: ap-northeast-1
Default output format [None]: json

確認コマンド
#

aws --version
terraform --version
git --version
hugo version
aws sts get-caller-identity  # AWS認証情報の確認

Hugoセットアップ
#

作業ディレクトリ構成
#

本記事では以下のディレクトリ構成で作業します。

作業ディレクトリ/
├── hugo-s3-demo/           # Hugoプロジェクト
└── hugo-s3-demo-infra/     # Terraformプロジェクト
    ├── backend-setup/
    └── prod/

サイト作成
#

hugo new site hugo-s3-demo
cd hugo-s3-demo

テーマ追加
#

Anankeテーマ(Hugo公式チュートリアルでも採用)を追加します。 git submoduleを使うことで、テーマの更新管理が容易になります。

git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke

hugo.toml編集
#

hugo.toml を編集してテーマを有効化します。

baseURL = 'https://example.org/'
languageCode = 'en-us'
title = 'Hugo S3 Demo'
theme = 'ananke'

baseURL は後でCloudFrontのドメインに変更します。

最初の記事作成
#

hugo new content posts/first-post.md

作成されたファイルを編集し、draft = false に変更して公開状態にします。

+++
date = '2025-12-14T14:59:51Z'
draft = false
title = 'First Post'
+++

これは最初の投稿です。

ビルド確認
#

# Hugoでビルド実行
hugo

# `public/` ディレクトリにHTMLファイルが生成されていればOK
ls public/
# 404.html  ananke  categories  images  index.html  index.xml  posts  sitemap.xml  tags

ローカルでプレビューしたい場合
#

hugo server

http://localhost:1313/ でプレビューできます。Ctrl+C で停止します。

Terraform state管理の準備
#

Terraformとは
#

TerraformはHashiCorpが開発したInfrastructure as Code(IaC)ツールです。 Go言語で実装されたOSSで、AWS・Azure・GCPなどのクラウドインフラをコードとして記述・管理できます。

本記事では、TerraformでS3バケットやCloudFrontディストリビューションを定義して、AWS環境を構築します。

Terraformを使うメリット
#

  1. 宣言的な記述 - 「こうなってほしい」状態を書くだけで構築できる
  2. スピードと安全性 - デプロイの自動化で高速かつ繰り返し実行可能
  3. ドキュメント化 - コード自体がインフラ構成のドキュメントになる
  4. バージョン管理 - Gitでインフラの変更履歴を追跡できる
  5. 再利用性 - 実績あるコードを再利用できる

VSCode拡張機能(推奨)
#

Terraformのコードを書く際は、VSCode拡張機能「HashiCorp Terraform」のインストールを推奨します。シンタックスハイライト、自動補完、フォーマットなどが利用できます。 マーケットプレイスへのリンク

なぜ先にstate管理環境を作るのか
#

Terraformはインフラの状態を「tfstate」というファイルで管理します。 このファイルをS3に保存し、チームで共有・ロック管理します。 tfstateによってTerraformのステータスなどを管理するので、 本体のインフラ環境とは別ディレクトリで先に作成します

  • tfstateを保存するS3バケットが必要
    • そのS3バケットをTerraformで作りたい
      • でもTerraform実行前にbackend(S3)が存在している必要がある → 最初にtfstate管理環境を構築する

ディレクトリ構成
#

hugo-s3-demo-infra/
└── backend-setup/
    └── main.tf

backend-setup/ でtfstate管理用のリソースを作成します。

mkdir -p hugo-s3-demo-infra/backend-setup
cd hugo-s3-demo-infra/backend-setup

backend-setup/main.tf
#

:::details 以下がmain.tfの全体となります。

terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

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

resource "random_id" "suffix" {
  byte_length = 4
}

resource "aws_s3_bucket" "tfstate" {
  bucket = "hugo-s3-demo-tfstate-${random_id.suffix.hex}"
}

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

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

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

resource "aws_dynamodb_table" "tfstate_lock" {
  name         = "hugo-s3-demo-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

output "s3_bucket_name" {
  value = aws_s3_bucket.tfstate.bucket
}

output "dynamodb_table_name" {
  value = aws_dynamodb_table.tfstate_lock.name
}

:::

コード解説
#

terraform / provider ブロック
#

terraform {
  required_version = ">= 1.0"
  required_providers {
    # AWSリソースを作成するためのプロバイダー
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    # ランダム文字列を生成するためのプロバイダー
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"  # 東京リージョン
}
  • terraform ブロック:Terraformのバージョンと使用するプロバイダーを指定
  • provider ブロック:AWSリージョンを東京(ap-northeast-1)に設定

random_id
#

# S3バケット名の一意性を確保するためのランダム文字列
resource "random_id" "suffix" {
  byte_length = 4  # 8文字の16進数を生成
}

S3バケット名はグローバルで一意である必要があります。 random_id でランダムな文字列を生成し、バケット名の末尾に付与します。

S3バケット
#

# tfstate保存用バケット
resource "aws_s3_bucket" "tfstate" {
  bucket = "hugo-s3-demo-tfstate-${random_id.suffix.hex}"
}

# バージョニング有効化(誤操作時に復元可能)
resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration {
    status = "Enabled"
  }
}

# サーバーサイド暗号化(tfstateには機密情報が含まれる可能性がある)
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# パブリックアクセスを完全ブロック
resource "aws_s3_bucket_public_access_block" "tfstate" {
  bucket                  = aws_s3_bucket.tfstate.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
リソース役割
aws_s3_buckettfstate保存用バケット
aws_s3_bucket_versioningバージョニング有効化(誤操作時に復元可能)
aws_s3_bucket_server_side_encryption_configuration暗号化(tfstateには機密情報が含まれる可能性がある)
aws_s3_bucket_public_access_blockパブリックアクセスを完全ブロック

DynamoDB
#

# tfstateロック用テーブル(同時実行を防止)
resource "aws_dynamodb_table" "tfstate_lock" {
  name         = "hugo-s3-demo-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"  # 使った分だけ課金
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"  # 文字列型
  }
}

tfstateのロック管理用テーブルです。 複数人が同時に terraform apply を実行した際の競合を防ぎます。

outputs
#

# 作成されたリソース名を出力(次のセクションで使用)
output "s3_bucket_name" {
  value = aws_s3_bucket.tfstate.bucket
}

output "dynamodb_table_name" {
  value = aws_dynamodb_table.tfstate_lock.name
}

作成されたリソース名を出力します。次のセクションでbackend設定に使用します。

実行
#

main.tf を作成したら実行します。

terraform init
#

terraform init

プロバイダー(AWS、random)がダウンロードされます。

Initializing the backend...
Initializing provider plugins...
- Installing hashicorp/aws v5.x.x...
- Installing hashicorp/random v3.x.x...

Terraform has been successfully initialized!

terraform plan
#

terraform plan

実行計画を確認します。実際のリソースは作成されません。

Plan: 6 to add, 0 to change, 0 to destroy.

6つのリソースが作成される予定であることを確認できます。

terraform apply
#

terraform apply

Enter a value: と表示されたら yes と入力します。

Apply complete! Resources: 6 added, 0 changed, 0 destroyed.

Outputs:

dynamodb_table_name = "hugo-s3-demo-tfstate-lock"
s3_bucket_name = "hugo-s3-demo-tfstate-xxxxxxxx"

:::message 出力値をメモしてください s3_bucket_namedynamodb_table_name は次のセクションで使用します。 :::

よく使うTerraformコマンド
#

本セクションで使用したコマンドをまとめます。

コマンド説明
terraform init初期化(プロバイダーのダウンロード)
terraform fmtコードのフォーマット整形
terraform validate構文チェック
terraform plan実行計画の確認(実際には変更しない)
terraform applyインフラの構築・変更
terraform destroyインフラの削除

基本的な流れは initplanapply です。

fmtvalidateplan は以下のように一気に実行できます。

terraform fmt && terraform validate && terraform plan

Terraform基盤構築
#

前のセクションでtfstate管理用のS3とDynamoDBを作成しました。 このセクションでは、実際にブログをホスティングするためのS3バケットとCloudFrontを構築します。

ディレクトリ構成
#

hugo-s3-demo-infra/
├── backend-setup/
│   └── main.tf          # tfstate用(作成済み)
└── prod/
    ├── versions.tf      # Terraform + Provider設定
    ├── backend.tf       # S3 backend設定
    ├── variables.tf     # 変数定義
    ├── main.tf          # S3, OAC, CloudFront
    └── outputs.tf       # 出力値

prod/ ディレクトリを作成します。

mkdir -p ../prod
cd ../prod

versions.tf
#

Terraformのバージョンとプロバイダーを設定します。

terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

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

backend.tf
#

tfstateをS3で管理するための設定です。 前のセクションで出力された値を使用してください。

terraform {
  backend "s3" {
    bucket         = "hugo-s3-demo-tfstate-xxxxxxxx"  # 出力されたs3_bucket_name
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "hugo-s3-demo-tfstate-lock"      # 出力されたdynamodb_table_name
    encrypt        = true
  }
}

:::message alert bucket の値は、前のセクションで出力された s3_bucket_name に置き換えてください。 :::

variables.tf
#

プロジェクト名を変数として定義します。

variable "project_name" {
  description = "Project name for resource naming"
  type        = string
  default     = "hugo-s3-demo"
}

main.tf
#

S3バケット、OAC、CloudFront Functionを作成します。

# バケット名の一意性を確保するためのランダム文字列
resource "random_id" "suffix" {
  byte_length = 4
}

# コンテンツ用S3バケット
resource "aws_s3_bucket" "content" {
  bucket = "${var.project_name}-content-${random_id.suffix.hex}"
}

# パブリックアクセスブロック
resource "aws_s3_bucket_public_access_block" "content" {
  bucket                  = aws_s3_bucket.content.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# CloudFront OAC(Origin Access Control)
resource "aws_cloudfront_origin_access_control" "main" {
  name                              = "${var.project_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# CloudFront Function(URLリライト)
resource "aws_cloudfront_function" "url_rewrite" {
  name    = "${var.project_name}-url-rewrite"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = <<-EOF
    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;
    }
  EOF
}

# CloudFront Distribution
resource "aws_cloudfront_distribution" "main" {
  origin {
    domain_name              = aws_s3_bucket.content.bucket_regional_domain_name
    origin_id                = "S3Origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.main.id
  }

  enabled             = true
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3Origin"
    viewer_protocol_policy = "redirect-to-https"
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized

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

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

# S3バケットポリシー(CloudFrontからのアクセスを許可)
resource "aws_s3_bucket_policy" "content" {
  bucket = aws_s3_bucket.content.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.content.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.main.arn
          }
        }
      }
    ]
  })
}

コード解説
#

OAC(Origin Access Control)
#

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

OACはCloudFrontからS3へのアクセスを制御する仕組みです。 S3バケットへの直接アクセスを禁止し、CloudFront経由のみに制限できます。

旧方式のOAI(Origin Access Identity)より細かい権限制御が可能で、AWSが推奨する新しい方式です。

CloudFront Function
#

resource "aws_cloudfront_function" "url_rewrite" {
  name    = "${var.project_name}-url-rewrite"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = <<-EOF
    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;
    }
  EOF
}

なぜCloudFront Functionが必要なのか?

OACを使用すると、S3の「静的ウェブサイトホスティング」機能が使えません。 そのため、/posts/first-post/ へのアクセスで index.html が自動補完されず、404エラーになります。

CloudFront Functionでviewer-requestイベント時にURLリライトを行い、この問題を解決します。

URLパターン変換後
/posts//posts/index.html
/posts/first-post/posts/first-post/index.html

CloudFront Distribution
#

resource "aws_cloudfront_distribution" "main" {
  # ...省略...
  
  default_cache_behavior {
    # HTTPをHTTPSにリダイレクト
    viewer_protocol_policy = "redirect-to-https"
    # AWS管理のキャッシュポリシー(静的サイトに最適)
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"
    
    # CloudFront Functionを関連付け
    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.url_rewrite.arn
    }
  }
  
  # デフォルト証明書を使用(カスタムドメインは#5で設定)
  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

cache_policy_id658327ea-f89d-4fab-a63d-7e88639e58f6 は、AWSが提供する「CachingOptimized」ポリシーのIDです。 静的サイトに最適化されたキャッシュ設定が適用されます。

S3バケットポリシー
#

resource "aws_s3_bucket_policy" "content" {
  bucket = aws_s3_bucket.content.id
  policy = jsonencode({
    # ...省略...
    Condition = {
      StringEquals = {
        "AWS:SourceArn" = aws_cloudfront_distribution.main.arn
      }
    }
  })
}

CloudFrontからのアクセスのみを許可するバケットポリシーです。 Condition で特定のCloudFront DistributionのARNを指定し、他のCloudFrontからのアクセスも拒否します。

outputs.tf
#

作成されたリソースの情報を出力します。

output "s3_bucket_name" {
  value = aws_s3_bucket.content.bucket
}

output "cloudfront_distribution_id" {
  value = aws_cloudfront_distribution.main.id
}

output "cloudfront_domain_name" {
  value = aws_cloudfront_distribution.main.domain_name
}

実行
#

5つのファイルを作成したら実行します。

# 初期化(backend設定を読み込み)
terraform init

# 実行計画の確認
terraform plan

# 構築
terraform apply

terraform apply の実行後、以下のような出力が表示されます。

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

cloudfront_distribution_id = "E2XXXXXXXXXX"
cloudfront_domain_name = "dxxxxxxxxxxxxx.cloudfront.net"
s3_bucket_name = "hugo-s3-demo-content-xxxxxxxx"

:::message 出力値をメモしてください s3_bucket_namecloudfront_domain_name は次のセクションで使用します。 :::

手動デプロイ
#

S3バケットとCloudFrontが作成されたので、Hugoでビルドしたコンテンツをデプロイします。

baseURLの更新
#

Hugoプロジェクトに戻り、hugo.tomlbaseURL をCloudFrontのドメインに更新します。

cd /path/to/hugo-s3-demo

hugo.toml を編集します。

baseURL = 'https://dxxxxxxxxxxxxx.cloudfront.net/'  # 出力されたcloudfront_domain_name
languageCode = 'en-us'
title = 'Hugo S3 Demo'
theme = 'ananke'

:::message alert baseURL は、前のセクションで出力された cloudfront_domain_name に置き換えてください。 末尾のスラッシュ / を忘れずに付けてください。 :::

ビルド
#

hugo

public/ ディレクトリにHTMLファイルが生成されます。

S3にアップロード
#

aws s3 sync public/ s3://hugo-s3-demo-content-xxxxxxxx/ --delete
  • s3://hugo-s3-demo-content-xxxxxxxx/ は出力された s3_bucket_name に置き換えてください
  • --delete オプションは、S3にあってローカルにないファイルを削除します

動作確認
#

ブラウザで https://dxxxxxxxxxxxxx.cloudfront.net/ にアクセスします。

以下を確認してください。

  • トップページが表示される
  • HTTPSで接続できる(ブラウザのアドレスバーに鍵マーク)
  • 記事詳細ページ(/posts/first-post/)が表示される

:::message 記事詳細ページが404になる場合は、CloudFront Functionが正しく動作していません。 Terraformコードの aws_cloudfront_function を確認してください。 :::

トラブルシューティング
#

403 Forbidden
#

原因: S3バケットポリシーが正しく設定されていない

対処法:

  1. terraform apply を再実行
  2. S3バケットポリシーがCloudFrontのARNを許可しているか確認

404 Not Found(サブページのみ)
#

原因: CloudFront Functionが動作していない

対処法:

  1. AWSコンソールでCloudFront Functionsを開く
  2. 関数が「Published」状態になっているか確認
  3. CloudFront Distributionに関連付けられているか確認

更新が反映されない
#

原因: CloudFrontのキャッシュが残っている

対処法: キャッシュを無効化します。

aws cloudfront create-invalidation \
  --distribution-id E2XXXXXXXXXX \
  --paths "/*"

--distribution-idterraform output で確認できます。

:::message キャッシュ無効化は月1,000パスまで無料です。 /* で全ファイルを無効化すると1パスとしてカウントされます。 :::

クリーンアップ(リソース削除)
#

記事の検証が終わったら、AWSリソースを削除してコストを抑えましょう。

prod環境の削除
#

cd hugo-s3-demo-infra/prod

# S3バケット内のファイルを削除(空でないと削除できない)
aws s3 rm s3://hugo-s3-demo-content-xxxxxxxx/ --recursive

# Terraformリソースを削除
terraform destroy

backend-setup環境の削除
#

cd ../backend-setup

# S3バケット内のファイルを削除
aws s3 rm s3://hugo-s3-demo-tfstate-xxxxxxxx/ --recursive

# バージョニング有効のため、DeleteMarkerも削除が必要な場合あり
# 削除に失敗した場合は、AWSコンソールからバケットを空にしてください

# Terraformリソースを削除
terraform destroy

:::message alert terraform destroy を実行すると、すべてのリソースが削除されます。 本番環境では十分注意してください。 :::

まとめ
#

本記事では、以下を構築しました。

  • Hugo: 静的サイトジェネレーターでブログを作成
  • S3: コンテンツの保存先
  • CloudFront: CDNでHTTPS配信
  • OAC: S3への直接アクセスを禁止
  • CloudFront Function: URLリライトで404を回避
  • Terraform: 全リソースをコード化

作成したリソース
#

リソース用途
S3バケット(content)Hugoビルド成果物の保存
S3バケット(tfstate)Terraformのstate管理
DynamoDBtfstateのロック管理
CloudFront DistributionCDN、HTTPS終端
CloudFront OACS3へのアクセス制御
CloudFront FunctionURLリライト
S3バケットポリシーCloudFrontからのアクセス許可

参考資料
#

Hugo-S3-CloudFrontで技術ブログを公開する - この記事は連載の一部です
パート 1: この記事