The bucket has been empty since the day it was built. Today, somebody finally puts something in it.
One file. Six lines of markup. A title, and a single sentence.
<html>
<head><title>Limitless Interactive</title></head>
<body>
Limitless Interactive is a private LLC headquartered in Tyler, TX.
</body>
</html>
Limitless Interactive is a private LLC headquartered in Tyler, Texas.
Eleven words.
Those eleven words now sit behind a certificate, a delivery network holding copies in data centers around the world, a signed handshake so nothing but that network can read from the storage, and a rule turning two countries away at the edge. A hundred and ninety lines of infrastructure, to serve one sentence.
Which is the correct trade. The sentence will change. The machinery around it will not have to.
And nobody uploads that file by hand. The page lives in the project, next to the code that serves it. When the build runs, it takes a fingerprint of the local file and compares it to what is published. Identical, leave it. Different, replace it.
resource "aws_s3_object" "index" {
for_each = var.s3_zones
bucket = aws_s3_bucket.single_page_website[each.key].id
key = "index.html"
source = "${path.module}/sites/${each.value.alias}.html"
acl = "public-read"
etag = filemd5("${path.module}/sites/${each.value.alias}.html")
}
The website becomes something you change by editing a file, not by dragging something into a browser window.
The rest of the day goes to names. The piece that did all of this was called public buckets, after the storage it happened to use. It becomes public web pages, after what it serves. And the piece running the machines in the back room drops the name of the scheduler inside it and becomes, simply, the homelab.
module "homelab" {
source = "./modules/homelab"
}
module "s3_public_html" {
source = "./modules/s3_public_html"
# include specific
s3_zones = { for k, v in var.route53_zones : k => v if contains(["limitless"], k) }
}
Neither rename changes any behavior. Both change what a person thinks when they open that folder later.
The old version had one company nailed into it end to end. Its domain, spelled out twice. Its zone identifier, twenty-two characters of noise, typed in twice more. Now it is a loop over a list. And the list says one thing. Include this one.
Not everyone except somebody. Not a filter you have to remember to update when a client leaves.
The bucket is not empty anymore. It holds a sentence.
The Ratio Looks Absurd
Welcome back.
I left you with a sentence. Eleven words, in a file six lines long, sitting behind more infrastructure than most companies put in front of an application.
Today I want to walk through all of it — because the ratio looks absurd, and it isn't.
The Bucket Was Empty
Start with the fact that hasn't changed since the storage was created: the bucket was empty.
Not misconfigured. Not broken. There was a bucket, correctly named after the company's domain, with permissions on it, a certificate pointed at it, and a global delivery network in front of it. Somebody typed that domain into a browser and got nothing, because nothing had ever been put inside.
That's a very common state for infrastructure work. You build the road first. The road is the hard part, the part with the failure modes and the twenty-minute waits. The content is something you'll do later, and later has a way of not arriving.
Today later arrives.
Inside the Project
The file is plain markup. A title. A body. One sentence naming the company, saying it's privately held, and saying where it's headquartered.
And the interesting thing isn't the sentence. It's where the file lives.
It lives inside the project. In a folder next to the code that builds the storage, the certificate, and the delivery network. It's in version control. It has a history. Changing the company's website is now an edit, a review, and a run of the same command that manages the mail system.
Three Lines
And the mechanism that gets it published is the part worth slowing down on, because it's three lines and it does something quietly excellent.
The upload is declared like any other piece of infrastructure. Here is a bucket, here is the name the object should have — index, dot, markup — and here is where to find the bytes on disk.
Then one more line. Take a fingerprint of the local file. A short hash that changes completely if any character in the file changes, and doesn't change at all if nothing does.
resource "aws_s3_object" "index" {
for_each = var.s3_zones
bucket = aws_s3_bucket.single_page_website[each.key].id
key = "index.html"
source = "${path.module}/sites/${each.value.alias}.html"
acl = "public-read"
etag = filemd5("${path.module}/sites/${each.value.alias}.html")
}
That fingerprint is compared against what's already published. Identical, and the tool does nothing. Different, and it replaces the object.
Exactly One Source
So the website stops being a place you upload things to and becomes a thing the project keeps synchronized.
Which kills the failure this whole style of work exists to kill. Somebody logs into a console at eleven at night, drags a corrected file into a browser window, and fixes the site. Nobody writes it down. Six months later the project runs, sees a page it doesn't recognize, and quietly puts the old one back.
That can't happen now, because there is exactly one source for what that page says, and it's the file in the repository.
And the path to that file is built from the row in the table. The module doesn't say which company's page to load. It says: load the page named after this row's alias.
source = "${path.module}/sites/${each.value.alias}.html"
What's Standing Behind It
Right. So that's the sentence. Now let's total up what's standing behind it.
A storage bucket, named for the domain. Ownership rules on it. A public access setting, deliberately switched off. A policy attached to the bucket. An access list. The published object itself.
Then a certificate, covering the domain and every name under it. Naming records to prove the domain is yours, so the certificate authority will issue it.
Then a delivery network — copies of that page held in data centers on several continents, so the page comes from somewhere near whoever asked for it.
Then a signing arrangement between the network and the storage. A rule turning away traffic from two countries at the edge. And finally a record at the root of the domain, pointing the bare name at the network.
Roughly a hundred and ninety lines. For eleven words.
The Correct Trade
And that is the correct trade, for a reason that's easy to state and easy to forget.
The sentence is going to change. Companies move, descriptions get rewritten, somebody eventually wants an actual page instead of a placeholder.
The machinery will not have to change with it. The certificate doesn't care what the page says. Neither does the delivery network, the signing, or the record at the root of the domain.
You build the expensive part once, when it's cheap to build because nothing depends on it yet. Then the content is a file you edit.
Doing it the other way around — putting up a quick page and adding the machinery later, under a deadline, while the site is live — is how you end up doing a certificate migration at four in the afternoon on a Friday.
Three Pieces of That Stack
Let me pull out three pieces of that stack, because each one is doing real work.
The Certificate
First, the certificate. It's issued for the domain, plus a wildcard covering everything underneath. And the way you prove you deserve it is the naming-system challenge — the authority hands you values, you publish them, it looks them up.
resource "aws_acm_certificate" "single_page_website" {
for_each = var.s3_zones
domain_name = each.value.domain
validation_method = "DNS"
subject_alternative_names = toset(["*.${each.value.domain}"])
lifecycle {
create_before_destroy = true
}
}
That used to be one hardcoded zone identifier. Now it's a loop: for every domain in the list, for every value the authority asks for, publish a record in that domain's own zone.
Two small settings on it are the kind of thing you only add after being burned. One says: when this certificate has to be replaced, create the new one before destroying the old one. Certificates are load-bearing — there is no acceptable window where the site has none.
The other says: if a validation record already exists, overwrite it rather than failing. Which sounds sloppy and isn't. These records get republished on every renewal, and the run that stops halfway because a record it was about to create is already there is a run that has told you nothing and cost you an afternoon.
resource "aws_route53_record" "single_page_website_dvo" {
depends_on = [aws_acm_certificate.single_page_website]
for_each = {
for item in flatten([
for key, zone in var.s3_zones : [
for dvo in aws_acm_certificate.single_page_website[key].domain_validation_options : {
domain_name = dvo.domain_name
hostedzone = zone.hostedzone
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
]
]) : item.domain_name => item
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = each.value.hostedzone
}
One Door
Second piece: the arrangement between the delivery network and the storage.
The obvious way to put a network in front of a bucket is to make the bucket readable by anybody and point the network at it. It works. It also means the storage is directly reachable, so all the things the network is doing for you — the certificate, the geographic rule, the caching — can be walked around by anyone who knows the storage address.
So there's a proper mechanism for this, and it's configured here. The network gets an identity, and it's told to sign every single request it makes to the storage, using the provider's signing scheme. Always — not when convenient.
resource "aws_cloudfront_origin_access_control" "single_page_website" {
for_each = var.s3_zones
name = each.value.domain
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
Which means the storage can be told to answer only requests carrying that signature. One door, and the network holds the key.
The Other Door Is Open
Although — and this is the honest reading of today's file — the other door is currently open too.
The bucket's public access protections are explicitly turned off. The policy on it grants read access to anyone. The published page is marked publicly readable.
resource "aws_s3_bucket_public_access_block" "single_page_website" {
for_each = var.s3_zones
bucket = aws_s3_bucket.single_page_website[each.key].id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "single_page_website" {
for_each = var.s3_zones
depends_on = [aws_s3_bucket_public_access_block.single_page_website]
bucket = aws_s3_bucket.single_page_website[each.key].id
policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "PublicReadGetObject",
"Effect" : "Allow",
"Principal" : "*",
"Action" : [
"s3:GetObject"
],
"Resource" : [
"arn:aws:s3:::${each.value.domain}/*"
]
}
]
}
)
}
So both paths to those bytes work right now: the signed one through the network, and the direct one to the storage.
The lock is installed. It hasn't been turned yet.
Which, for a page containing eleven words about where a company is headquartered, is a completely defensible place to stop. The machinery is in place and the tightening is a two-line change on the day it matters.
The Rule at the Edge
Third piece, and it's the one people find surprising: the rule at the edge.
The delivery network is told to refuse requests from two countries outright. Not redirect them. Not rate-limit them. Refuse.
restrictions {
geo_restriction {
locations = ["CN", "RU"]
restriction_type = "blacklist"
}
}
And "at the edge" is the important half of that. The refusal happens at whichever data center the request landed in, which is nowhere near the storage. Nothing reaches the bucket. Nothing is served. Nothing is billed.
For a small company with no customers in those places, that removes a large fraction of automated traffic — scanners, scrapers, and probes — before it becomes anybody's problem.
It's also worth naming that it's a blunt instrument. Geography is not intent, and anybody determined enough routes around it in about ten seconds. What it's actually good at is turning down the volume, and turning down the volume is most of what a small site needs.
The Cache Fractures
The caching settings tell the same kind of story if you read them.
The network holds a copy for an hour by default, up to a day at most. Anything arriving unencrypted gets redirected to the encrypted address rather than refused, which is the friendly choice.
And two things are explicitly not passed through to the storage: query strings, and cookies.
That's not laziness — it's the setting that makes caching work at all. If query strings were forwarded, the network would treat every variation as a different page and hold a separate copy of each. Someone appends a tracking parameter and the cache fractures into a thousand identical copies of one sentence.
The page's answer doesn't depend on any of that, so none of it is forwarded, and every visitor in a region shares one cached copy.
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
The network is also told to answer over the modern addressing scheme as well as the old one, which costs nothing and quietly means the site works from networks that only speak the new numbers.
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
The Bare Name
And then the last piece of the chain, which is the one that makes the bare domain work at all.
Somebody types the company's name into a browser with nothing in front of it. No triple-w, no path. Just the domain.
Two separate things have to be true for that to produce a page.
The first is that the root of the domain has to point at the delivery network. Which sounds routine and isn't — the naming system does not let you put an ordinary alias at the very top of a domain. It's a rule from the original design, and it's why so many sites quietly redirect you to a name with something in front of it.
The provider's answer is a special record type that behaves like an address at the root but resolves like an alias, following the network wherever it moves. That's what's written here, and it's why the bare name works.
resource "aws_route53_record" "single_page_website" {
for_each = var.s3_zones
zone_id = each.value.hostedzone
name = ""
type = "A"
alias {
name = format("%s.", aws_cloudfront_distribution.single_page_website[each.key].domain_name)
zone_id = aws_cloudfront_distribution.single_page_website[each.key].hosted_zone_id
evaluate_target_health = false
}
}
The Default Document
The second thing is smaller and easier to miss.
A request for the root of a site doesn't name a file. It just asks for the domain. So the network has to be told which object counts as "the page" when nobody asked for one specifically.
One setting does that — the default document is the index file. Leave it out and the same request produces an error, or a listing of what's in the bucket, depending on how things are configured.
It's one line. It's also the difference between a working website and a certificate in front of nothing.
Named After the Job
Now — the other half of today, which touched no behavior at all and is arguably the more valuable work.
Two modules got renamed.
The piece that does everything I just described was called public buckets. Named after the storage service it happened to use. Today it becomes public web pages — named after the job it does.
And the piece managing the machines in the back room was named after the scheduler running inside it. Today it drops that name entirely and becomes, simply, the homelab.
Neither rename changes a single resource. The files moved; the contents didn't. Run the project before and after and the cloud looks identical.
A Name That's a Lie
So why spend a day on it?
Because a module named after its implementation is a promise you'll eventually break. Call it "buckets" and the day you put a page somewhere that isn't a bucket, the name is a lie. Call it after the scheduler, and the day the back room runs something the scheduler doesn't manage — which it already does — the name is a lie.
And a name that's a lie costs you the thing folder names exist for. Somebody who wants to change the website has to open two or three folders before they find the right one, and each wrong guess makes them slightly less willing to trust the layout next time.
Names describing the job survive changes in how the job is done. Names describing the tool don't.
This is also the cheapest possible moment to do it. Renaming a module with two things in it is a rename. Renaming a module with forty things in it is a project.
Copied Out of a Tutorial
And there's a small piece of counter-evidence sitting right in the middle of the renamed file, which I enjoy far more than I should.
The delivery network needs an internal label for where it fetches content from — a handle, used in two places, that never appears anywhere a human will see it.
That label is a string. And the string is the one from the provider's example documentation. Capital letters in the middle, the storage service's name embedded in it, the whole thing looking exactly like what it is: something copied out of a tutorial two years ago and never touched again, because it works and nothing depends on it reading well.
locals {
s3_origin_id = "myS3Origin"
}
So on the same day that two folders get renamed to describe their jobs instead of their tools, a leftover from a getting-started guide survives untouched in the middle of one of them.
Which is not hypocrisy. It's the correct allocation of attention. Folder names are read by people trying to find things. That label is read by nobody. The work went where the confusion was.
A Row, Not a Rewrite
And there's a companion to that, inside the module the rename opened up.
The old version had one company nailed into it end to end. The domain spelled out as a literal in the bucket name, and again in the certificate, and again in the list of names the network answers to. The zone identifier — twenty-two characters of noise — typed in twice.
Today it's a loop over a list, exactly like the mail work. Nothing in the file names a company.
resource "aws_s3_bucket" "single_page_website" {
for_each = var.s3_zones
bucket = each.value.domain
}
Which means the second static site is now a row, not a rewrite.
A Default of No
But the way that list gets built is the sharpest decision of the day, and it's one line.
Yesterday, when the mail modules were handed their lists, the filter was written as an exclusion. Take every row in the table, and drop the ones that shouldn't be here.
Today's filter is written the other way. Take every row in the table, and keep the ones on this list. And the list has exactly one name on it.
module "workmail" {
source = "./modules/workmail"
# exclude specific
workmail_zones = { for k, v in var.route53_zones : k => v if !contains(["flywheel"], k) }
}
module "s3_public_html" {
source = "./modules/s3_public_html"
# include specific
s3_zones = { for k, v in var.route53_zones : k => v if contains(["limitless"], k) }
}
Those two look equivalent. They are not remotely equivalent.
An exclusion list is a default of yes. Everything is included unless somebody remembered to name it. So the moment somebody adds a row to the table for a new domain, it silently acquires whatever that module builds — and the person adding the row may have no idea the module exists.
An inclusion list is a default of no. A new row gets nothing until somebody names it.
For the mail modules, the exclusion default is defensible. Adding a domain that accidentally gets a corporate directory is expensive and annoying, but it's inward-facing.
This module publishes bytes to the entire internet under a company's name, with a certificate proving they're genuine. That is not a thing you want any row in a table to be able to switch on by existing.
So the register can grow — clients, automation, domains nobody's thought of yet — and none of them get a public website until somebody types their name on one specific line.
Silence Means No
So let's total up the day.
A file finally went into a bucket that had been empty since it was built. It went in as infrastructure — fingerprinted, version-controlled, republished only when it actually changes, and impossible to quietly overwrite from a browser.
Two folders got names that describe their jobs instead of their parts, at the only moment when that's a five-minute change. The static hosting stopped knowing which company it was built for. And the switch that turns it on for anybody else got wired so that silence means no.
None of which is visible from outside. Type that domain into a browser today and you get a title bar and a sentence.
But the sentence arrives over an encrypted connection, from a data center near you, holding a copy that was placed there because a fingerprint in a repository didn't match what was published.
The bucket is not empty anymore. It holds a sentence — and everything that gets that sentence to you is now something you change by editing a file.