hosoitoru.jp のインフラとデプロイの設計 — 細井 透(Toru Hosoi)
静的 HTML を AWS で配信するだけの、シンプルな構成です。S3 + CloudFront(OAC)でホスティング、GitHub Actions(OIDC)で main push のたびに自動デプロイ、インフラは全部 Terraform。
S3 バケットはパブリックアクセスを全部ブロックしています。直接 URL を叩くと 403。CloudFront 経由でしか到達できない作り。
CloudFront から S3 へのアクセス制御には OAC(Origin Access Control)を使っています。OAI(Origin Access Identity)は旧世代で、AWS も OAC を推奨しています。OAC は SigV4 署名でリクエストを検証するので、より堅牢です。
さらにバケットポリシーで AWS:SourceArn を特定の CloudFront ディストリビューション ARN に縛って、同じアカウント内の別ディストリビューションからも読めないようにしています。最小権限の原則に沿った形です。
ACM でワイルドカード証明書(hosoitoru.jp + *.hosoitoru.jp)を発行。サブドメインを後から足すときに、証明書を再発行しなくていいのが地味に助かる。
CloudFront 用の ACM 証明書は us-east-1 でしか発行できないという独特の仕様があるので、Terraform 側で provider "aws" { region = "us-east-1" } を別エイリアスで定義してます。
S3 はバケットが非公開だと存在しないパスにも 403 を返してくる。それを CloudFront で 404 に変換して、カスタムの 404 ページに飛ばしています。
デプロイのために IAM User は作っていません。アクセスキーを GitHub Secrets に置きたくなかったのが理由。漏れたら永続的に悪用されるので。
GitHub Actions は実行時に OIDC トークン(JWT)を発行してくれます。それを使って sts:AssumeRoleWithWebIdentity を呼び、一時的に IAM Role を assume する、という流れ。
必要最小限にしてます。
PutObject / GetObject / DeleteObject / ListBucket(デプロイ対象バケットのみ)CreateInvalidation / GetInvalidation(対象ディストリビューションのみ)S3・CloudFront・Route 53・ACM・IAM(OIDC)まで全部 Terraform で。
S3 バックエンド + use_lockfile(Terraform 1.10 以降のネイティブ S3 ロック)で管理してます。
backend "s3" {
bucket = "hosoitoru-jp-tfstate"
key = "infra/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
以前は DynamoDB テーブルでロックしてましたが、S3 ネイティブのロックに切り替えました。DynamoDB テーブルの管理も、そのための IAM 権限付けも要らなくなって構成がシンプルに。
CloudFront に紐付ける ACM 証明書は us-east-1 でしか発行できない、という独特の制約があります。S3 など他のリソースは東京(ap-northeast-1)に置きたいので、ACM のためだけに provider "aws" をもう 1 つ us-east-1 エイリアスで定義して、そのリソースにだけアタッチしています。
ルートとワイルドカードの ACM 検証 CNAME が同じ値になることがあるので、for_each + ...(spread)で重複を落としてから Route 53 に登録しています。これをやらないと Terraform が同一レコードを二重登録しようとしてコケる。
変数 route53_zone_id が空のときだけ count = 1 でゾーンを作る形にしています。既存ゾーンがあれば ID を渡すだけでよく、このモジュールを他プロジェクトに持っていっても動くようになっています。
main に push すれば終わり。手作業なし。
aws s3 sync で差分だけ S3 にアップロード/* を invalidate