Infrastructure and deployment design for hosoitoru.jp — by Toru Hosoi
A simple setup: static HTML served from AWS. S3 + CloudFront (OAC) for hosting, GitHub Actions (OIDC) for auto-deploy on every push to main, and Terraform for all of the infrastructure.
The S3 bucket has public access fully blocked. Any direct URL hits return 403. The only way in is through CloudFront.
I use OAC (Origin Access Control) for CloudFront-to-S3 access, not the older OAI (Origin Access Identity). AWS recommends OAC for new deployments. It verifies requests via SigV4, which makes the access control stronger.
The bucket policy pins AWS:SourceArn to the specific CloudFront distribution ARN, so even another distribution in the same account can't read from this bucket. This follows the principle of least privilege.
I issue a wildcard certificate (hosoitoru.jp + *.hosoitoru.jp) in ACM so future subdomains don't need a re-issuance.
ACM certificates for CloudFront must be in us-east-1. I handle this by defining a second AWS provider alias in Terraform: provider "aws" { region = "us-east-1" }.
S3 returns 403 for non-existent paths (because the bucket is private). CloudFront rewrites those to 404 and serves a custom 404 page.
There is no IAM User for deploys. I didn't want long-lived access keys sitting in GitHub Secrets — if one leaks, the attacker has persistent access.
GitHub Actions issues an OIDC token (JWT) at runtime. The workflow uses it to call sts:AssumeRoleWithWebIdentity and temporarily assume an IAM Role.
Scoped to the bare minimum:
PutObject / GetObject / DeleteObject / ListBucket on the deploy bucket onlyCreateInvalidation / GetInvalidation on the target distribution onlyS3, CloudFront, Route 53, ACM, and IAM (OIDC) — all managed in Terraform.
S3 backend with use_lockfile (native S3 locking, Terraform 1.10+).
backend "s3" {
bucket = "hosoitoru-jp-tfstate"
key = "infra/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
Previously ran DynamoDB-backed locking; moved to the native S3 lock to drop the extra table and its IAM permissions. Simpler config.
The ACM certificate attached to CloudFront has to be issued in us-east-1 — a quirky CloudFront constraint. Everything else lives in ap-northeast-1 (Tokyo), so I define a second provider "aws" with a us-east-1 alias and attach it only to the ACM resources.
Validation CNAMEs for the root domain and wildcard sometimes coincide. I run them through for_each with ... (spread) to dedupe before registering with Route 53 — otherwise Terraform errors on duplicate records.
If route53_zone_id is empty, I create a new zone with count = 1. If an existing zone ID is passed in, it's reused. The same module drops into other projects without changes.
A push to main is the whole deploy. No manual steps.
aws s3 sync uploads the diff to S3/*