AWSで個人ブログを立ち上げた話 〜Route53・ACM・CloudFront・S3・Terraform・GitHub Actionsまで〜
はじめに
AWSエンジニア(Jr)の備忘録として、個人ブログサイトをAWSのサービスだけで構築した。 ドメイン取得からインフラのコード化、CI/CDでの自動デプロイまで一通りやってみたので、詰まったポイントも含めて記録しておく。
全体構成
最終的に出来上がった構成はこうなった。
ユーザー
↓
Route 53(ドメイン管理・DNS)
↓
CloudFront(CDN配信・HTTPS終端)
↓
S3(静的サイトホスティング)デプロイの自動化は以下の流れ。
ローカルでMarkdown記事を書く
↓ git push
GitHub Actions が起動
↓ Hugoでビルド
↓ OIDCでAWS認証
S3にアップロード
↓
CloudFrontキャッシュクリア
↓
本番反映使用した主なAWSサービスは以下の5つ。
- Route 53 — ドメイン取得・DNS管理
- ACM — SSL/TLS証明書(無料)
- S3 — 静的サイトのファイル置き場
- CloudFront — CDN配信・HTTPS化
- IAM(OIDC) — GitHub ActionsからAWSへの安全な認証
ドメイン取得とRoute 53
ドメインは お名前.com で seiyoudako.com を取得した。Route 53には自分でドメインを取得する機能もあるが、今回は既に持っていたお名前.comのドメインを使う形にしたので、Route 53側にはホストゾーンだけを作成し、お名前.com側のネームサーバー(NS)をRoute 53のものに向ける設定(NS委任)を行った。
NSの伝搬には多少時間がかかるが、完了すればRoute 53がそのドメインのDNSを管理できるようになる。
タグ付けのルール
案件でよく使われる形式を参考に、リソースには以下のようなタグを付けることにした。
Name = "com-blog-<サービス種別>"環境(com/dev)とプロジェクト(blog)が一目でわかるようにしている。
S3バケットの作成
静的サイトのコンテンツを置くバケットを作成した。バケット名はそのままドメイン名 seiyoudako.com にした。パブリックアクセスは全てブロックしたままにして、CloudFront経由でのみアクセスできるようにする方針にした。
ACMでSSL証明書を発行
CloudFrontで独自ドメインをHTTPS化するには、ACMで証明書を発行する必要がある。ここで最初につまずいたのがリージョンの制約だった。
CloudFrontに使う証明書は必ず us-east-1(バージニア北部)リージョンで発行しなければならない。他のリージョンで発行した証明書はCloudFrontにアタッチできない。
証明書発行後は「保留中」の状態になり、DNS検証が必要になる。ACMの証明書詳細画面で発行される検証用のCNAMEレコードを、Route 53のホストゾーンに追加することで、数分〜30分程度で「発行済み」に変わる。ACMコンソールの「Route 53でレコードを作成」ボタンを押せば自動でCNAMEが追加されるので手動でコピペする必要はなかった。
CloudFrontディストリビューションの作成
CloudFrontのディストリビューションを作成し、S3をオリジンに設定した。ここでもいくつか詰まった点があった。
詰まりポイント①:組み込みWAFの「無料プラン」
ディストリビューション作成時、セキュリティ設定で組み込みのWAF保護が有効になっていることに気づいた。「無料プランに含まれる」という表示だったが、過去に別サイトでAWS WAFを使ってコストが発生した経験があったため不安になった。
調べたところ、これは2025年11月にAWSが新しく出したCloudFrontフラットレートプランの Free プラン($0/月)で、WAF・DDoS保護・Route 53 DNS・TLS証明書・100GBのデータ転送が含まれているものだった。従来の個別課金のAWS WAF(WebACLを作るタイプ、月$5〜)とは別物で、この意味では無料表示は正しかった。
ただし個人ブログではWAFが不要と判断し、最終的にはCloudFrontの「プランのキャンセル」機能でフラットレートプランを解除し、純粋な従量課金(無料枠内でほぼ$0)の構成に戻した。
詰まりポイント②:ACMをアタッチしても反映されない
証明書をCloudFrontにアタッチした後、curl -I https://seiyoudako.com を実行しても Could not resolve host エラーが出た。原因はRoute 53側にAレコード(Aliasレコード)が未設定だったこと。
Route 53でAliasレコードを作成する際、CloudFrontディストリビューションを選択肢に出すには**「米国東部(バージニア北部)」を明示的に選択する必要がある**という仕様があり、ここでも少しつまずいた。AレコードとAliasレコードは別物ではなく、AレコードにAliasフラグを立てたものがAliasレコードという整理も併せて理解した。
詰まりポイント③:Route 53にディストリビューションが出てこない
Aliasレコード作成時、対象のCloudFrontディストリビューションが選択肢に出てこない問題も発生した。原因は、CloudFront側の「代替ドメイン名(CNAME)」に seiyoudako.com が登録されていなかったこと。
Route 53は「そのディストリビューションが該当ドメインのリクエストを処理する」と宣言されていないと候補に出さない仕様になっている。CloudFront側でドメインを追加登録することで解決した。
詰まりポイント④:HTTPSは繋がるがトップページが403
証明書アタッチ・Aliasレコード設定後、HTTPS接続はできるようになったが、HTTPアクセスで403が返るようになった。これはCloudFrontがHTTPを拒否している状態で、正常な挙動だった。
S3にコンテンツを配置してdefault_root_objectを設定
最初、S3バケットは初期状態のままだったため seiyoudako.com にアクセスするとS3のデフォルト画面が表示されていた。シンプルなindex.htmlをアップロードしてCloudFrontのキャッシュをクリアしても表示が変わらず、原因はCloudFrontの default_root_object が未設定だったこと。これを index.html に設定することで解決した。
Terraformでインフラをコード化
ここまでマネジメントコンソールで構築してきたインフラを、Terraformでコード管理する作業に移った。
ディレクトリ構成
将来的に検証環境(dev)と本番環境(com)を分けられるよう、以下の構成にした。
terraform/
├── com/
│ ├── backend.tf # S3バックエンド定義
│ ├── provider.tf # プロバイダー設定
│ ├── main.tf # モジュール呼び出し
│ └── variables.tf # 環境変数
├── dev/ # 将来用
└── modules/
├── s3/
├── cloudfront/
├── route53/
├── acm/
└── iam/comとdevで共通のモジュールを呼び出し、環境固有の値だけ変数で渡す設計にした。
tfstateの管理
tfstateはS3で管理することにした。バックエンド用のバケットは事前に手動で作成する必要がある(Terraform自身が管理する対象ではないため)。
aws s3api create-bucket --bucket seiyoudako-tfstate --region ap-northeast-1 \
--create-bucket-configuration LocationConstraint=ap-northeast-1
aws s3api put-bucket-versioning --bucket seiyoudako-tfstate \
--versioning-configuration Status=Enabledbackend.tfでは変数(var.prefixなど)が使えないという制約があるため、keyの値はベタ書きにした。Terraformはバックエンド初期化をリソース評価より先に行うため、変数が解決される前にbackendの設定が読まれるのが理由。
既存リソースのインポート
マネコンで作ったリソースをTerraformの管理下に置くため、terraform import ブロックと -generate-config-out オプションを使った。
import {
to = aws_s3_bucket.this
id = "seiyoudako.com"
}terraform plan -generate-config-out=generated.tfここでの学びは、-generate-config-out はルートモジュールのリソースにしか使えないということ。最初モジュール内のリソースを対象にimportしようとしてエラーになったため、一度ルートモジュールでコード生成してから、terraform state mv コマンドでモジュール内のリソースパスに移動させるという2段階の手順を踏んだ。
terraform state mv aws_s3_bucket.this module.s3.aws_s3_bucket.blog_s3また、ACM証明書はus-east-1のリソースのため、importの際もプロバイダーエイリアス(provider = aws.us_east_1)を明示する必要があった。
命名規則の整理
リソース名は最終的に this のような汎用名ではなく、タグ名と揃えた意味のある名前(blog_s3, blog_cloudfrontなど)にした。Terraformのリソース名にはハイフンが使えず、アンダースコアのみ有効という制約があることもここで学んだ。
GitHub Actionsでの自動デプロイ
インフラが整ったところで、記事を書いてgit pushすれば自動でサイトに反映される仕組みを作った。
静的サイトジェネレーター Hugo の導入
最初は「index.html一枚でブログを作る」イメージだったが、記事ごとにURLが分かれる本格的なブログにするには静的サイトジェネレーターが必要という指摘を受け、Hugoを導入した。
brew install hugo
hugo new site blog --force
cd blog
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/anankeMarkdownで記事を書き、Hugoがそれをビルドして静的HTMLを生成する。生成されたHTML群をまるごとS3にアップロードするという役割分担になっている。テーマは自作・改造することも可能で、UIを完全に自分でカスタマイズすることもできる。
OIDCによるAWS認証
GitHub ActionsからAWSへのアクセスには、長期間有効なIAMアクセスキーを使う方法もあるが、よりセキュアな OIDC(OpenID Connect) を使う方針にした。IAMアクセスキーをGitHub Secretsに置かずに済むのがメリット。
Terraformで以下の3つのIAMリソースを作成した。
- OIDCプロバイダー(
aws_iam_openid_connect_provider)— GitHubのトークン発行元を信頼する設定 - IAMロール(
aws_iam_role)— GitHub Actionsが引き受けるロール。信頼ポリシーで対象リポジトリを限定 - IAMポリシー(
aws_iam_policy)— S3へのアップロードとCloudFrontのキャッシュクリア権限
管理をしやすくするため、OIDC・ロール・ポリシーはそれぞれ別ファイル(oidc.tf, role.tf, policy.tf)に分けた。
詰まりポイント:OIDC認証が通らない
deploy.ymlを書いてpushしたところ、以下のエラーで失敗し続けた。
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity原因を切り分けるため、以下を順番に確認した。
- IAMロールの信頼ポリシー →
sub条件は合っていたが、aud(audience)条件が抜けていた。StringEqualsで"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"を追加。 - OIDCプロバイダーのthumbprint → GitHub側のTLS証明書のフィンガープリントが記事の値と異なっていた。
opensslコマンドで最新の値を取得し直して更新。 - GitHubリポジトリのOIDC設定 → 最終的な原因はここだった。GitHubの「Settings → Actions → OIDC」で immutable subject claim という機能が有効になっており、2026年7月15日以降に作成されたリポジトリでは、subjectクレームのフォーマットが
repo:<owner>@<owner_id>/<repo>@<repo_id>という新しい形式に変わっていた。IAMロールの信頼ポリシーのsub条件をこの新形式に合わせて修正することで解決した。
複数の要因が絡んでいたため、ひとつずつ仮説を検証しながら潰していく必要があった。
サブページが表示されない問題
デプロイ自体は成功したが、トップページ以外の記事ページ(例:/posts/hello-world/)にアクセスするとAccessDeniedのXMLエラーが表示された。
原因はCloudFrontのdefault_root_objectがルートディレクトリにしか適用されないため。サブディレクトリではindex.htmlが自動補完されない仕様だった。
解決策として CloudFront Functions を使い、リクエストURIの末尾を見てindex.htmlを補完するJavaScript関数を作成し、viewer-requestイベントにアタッチした。
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;
}これをTerraformのaws_cloudfront_functionリソースとして定義し、terraform applyで適用したところ、サブページも正常に表示されるようになった。
完成した構成
最終的に以下がすべて動く状態になった。
seiyoudako.comにHTTPSでアクセス可能- Markdown記事を書いて
git pushするだけで自動デプロイ - インフラは全てTerraformでコード管理(
terraform planでNo changesを確認できる状態) - GitHub ActionsはOIDCでAWSに安全に認証
- サブページも含めて正常表示
振り返り
一つ一つのAWSサービス自体はドキュメント通りに進めれば動くものが多いが、サービス間の連携部分(Route 53とCloudFrontのAliasレコード、CloudFrontとACMのリージョン制約、GitHub ActionsのOIDCとIAMの信頼ポリシー)でつまずくことが多かった。
特にOIDC認証まわりは、GitHub側の仕様変更(immutable subject claim)のようなドキュメントだけでは気づきにくい最新の変更が絡んでいたため、公式のエラーメッセージを起点に一つずつ仮説検証していくプロセスが重要だと感じた。
今後はテーマのカスタマイズや、dev環境の追加などを進めていく予定。