Business Challenge
State is created in the first hour of a project by someone following a quickstart, and every one of these arrives later.
It is a credential store. Google states that there are many resources and data providers in Terraform that store secret values in plaintext in the state file, and the guidance that follows is unambiguous: make sure that only the build system and highly privileged administrators can access the bucket used for remote state.
Correct approachGrant access to that bucket on the same basis you would grant access to Secret Manager. A read on the state bucket is a read on whatever secrets the configuration has ever produced.
It keeps things simple until the first refresh takes twenty minutes and the first mistake is estate-wide. Google puts a number on it: do not include more than 100 resources in a single state, and ideally only a few dozen.
Correct approach
Treat the state boundary as the blast radius it is. Split the configuration into a modules directory holding the actual configuration and an environments directory holding a root configuration per environment.
Pinned how tightly is the question. Google's recommendation is specific: in root modules, declare each provider and pin to a minor version, which allows automatic upgrade to new patch releases while still keeping a solid target. Pin to a patch and you own every upgrade by hand; leave it open and a provider release changes your plan without a commit.
Correct approachPin the minor, let patches float, and review the pins on a schedule rather than when something breaks.
Then that resource is managed through Google Cloud's beta APIs, which is what the provider exists for — Google's description is exactly that: use it to provision and manage Google Cloud beta APIs. The consequence worth knowing is that the API surface underneath a beta resource carries beta's stability guarantees, not GA's.
Correct approach
Use google-beta deliberately and per resource, record why, and revisit when the feature reaches GA. A beta dependency that nobody remembers taking is the one that surprises you.
Architecture
Use the Terraform provider for Google Cloud to provision and manage Google Cloud infrastructure. Two decisions inside that sentence outlive everything else you write: which provider, and where the state goes.
Two providers, one configuration
Google describes them in a line apiece. The google provider is used to provision and manage Google Cloud APIs; the google-beta provider is used to provision and manage Google Cloud beta APIs. They coexist in one configuration, chosen per resource.
The architectural point is that this is a dependency decision rather than a syntax one. Reaching for google-beta is how a preview feature enters a production configuration, and it does so quietly — one extra line on one resource, no separate approval, and nothing that reports it later. Post #24's advice about feeds applies here too: the thing to write down is the reason, in the place a future reader will look.
The state bucket, and why each setting is there
Google's recommended bucket is four decisions, and each has a stated purpose worth reading rather than copying:
| Setting | Google's reason |
|---|---|
versioning enabled |
To ensure that earlier versions of the state are preserved in the bucket. |
force_destroy = false |
To ensure that the bucket is not deleted if there are objects in it. |
public_access_prevention = enforced |
To make sure the bucket contents aren't accidentally exposed to the public. |
uniform_bucket_level_access = true |
To allow controlling access to the bucket and its contents using IAM permissions instead of access control lists. |
Versioning is the one that earns its cost. A corrupted or truncated state is recoverable only if a previous version exists, and Google notes the storage cost can be mitigated with Object Lifecycle Management rather than by turning versioning off. force_destroy = false is the same instinct as the liens in post #7 — a deliberate obstacle in front of the destructive path.
public_access_prevention and uniform_bucket_level_access both appeared in the day-one organization policy set, and the state bucket is usually the first bucket an estate creates. Two consequences follow. If those policies are enforced at the organization, the recommended state bucket already complies and nothing surprises you. And post #22's warning applies in full here: uniform bucket-level access cannot be disabled after 90 consecutive days on a bucket, so this is a decision with a clock, taken on the most important bucket you own.
The hundred-resource ceiling
Do not include more than 100 resources in a single state, and ideally only a few dozen. That is unusually concrete guidance and it is really a statement about three things at once: how long a plan takes, how much a single mistaken apply can destroy, and how many people have to coordinate before anyone can change anything.
The structure Google recommends follows from it — a modules directory holding the actual configuration for a service, and an environments directory holding the root configuration for each environment. One state per environment per component, rather than one state per estate.
Why This Architecture Holds Up
Plaintext, by design, in ordinary resources
This is the part most worth internalising, because nothing in the workflow announces it. Google states that many resources and data providers in Terraform store secret values in plaintext in the state file, and advises avoiding storing secrets in state where possible. The named examples are not exotic: service account keys, TLS private keys, and client configuration among them.
So the state file is a record of infrastructure that happens to accumulate credentials as a side effect. Nobody chooses to put a private key in a Cloud Storage bucket; it arrives because a resource that generates one writes its result to state, and state goes where the backend points.
Google's instruction is that only the build system and highly privileged administrators should access the bucket used for remote state. Read alongside the plaintext-secrets statement, that stops being a general hardening tip and becomes the specific access model for a credential store. A developer who can read the state bucket to debug a plan can read every secret the configuration has produced. Grant it the way you would grant Secret Manager access — and note that post #23's asset export makes bucket IAM queryable, which is how you find out who currently has it.
Three defences, in order of how much they help
- Keep secrets out. Where possible, avoid storing them in state at all — reference Secret Manager through data sources rather than generating credentials as Terraform resources. This is the only defence that removes the problem rather than containing it.
- Restrict the bucket. Build system and highly privileged administrators only, enforced through IAM under uniform bucket-level access.
- Add encryption beyond the default. Google Cloud buckets are encrypted at rest, and customer-supplied encryption keys provide an added layer of protection. Useful, and note where it sits in this list — it protects against a threat model that the first two have usually already addressed.
Remote state gets designed; the copy on a laptop does not. Google's advice is direct: to prevent accidentally committing development state to source control, use gitignore for Terraform state files. It is the smallest item in this post and probably the most frequently violated, because it is a one-line omission in a repository created before anybody thought about state at all.
What state is, and what it is not
Posts #23 and #24 were about discovering what exists. State is a different claim: it is what Terraform believes exists, recorded at the last apply. The gap between the two is drift, and it is why the asset export matters even in a fully Terraformed estate — the state file cannot tell you about a resource created in the console, and it will happily plan a change against a resource that somebody has already altered underneath it.
Neither file is authoritative about the other. Asset Inventory knows what is there; state knows what was intended. Reconciling them is the actual job, and it needs both.
Key Architecture Decisions
| Decision | Choose this | Because |
|---|---|---|
| Who can read the state bucket | Build system and highly privileged admins only | Google's own instruction, and state holds secret values in plaintext. |
| Secrets in configuration | Reference Secret Manager, do not generate them | Where possible, avoid storing secrets in state — the only defence that removes the problem. |
| Size of a single state | Under 100 resources, ideally a few dozen | Google's stated ceiling, and the state boundary is the blast radius. |
| Repository layout | modules and environments directories |
The recommended split, and it produces one state per environment naturally. |
| Provider version | Pin the minor version | Allows automatic patch upgrades while still keeping a solid target. |
Using google-beta |
Per resource, with the reason recorded | It manages Google Cloud beta APIs, so the stability guarantee changes. |
| Bucket versioning | On, with lifecycle rules for cost | Earlier versions of the state are preserved; a corrupted state is otherwise unrecoverable. |
| Bucket deletion | force_destroy = false |
Ensures the bucket is not deleted if there are objects in it. |
| Bucket exposure | public_access_prevention = enforced |
Makes sure the contents aren't accidentally exposed to the public. |
| Bucket access control | uniform_bucket_level_access = true |
IAM instead of ACLs — and irreversible after 90 days, so decide once. |
| Local state files | gitignore them on day one | Prevents accidentally committing development state to source control. |
| Extra encryption | Customer-supplied keys, after the first two defences | Buckets are already encrypted at rest; this is an added layer, not the primary control. |
Closing Thought
Almost every decision in this post is made in the first hour of a project, by whoever is setting up the repository, usually from a quickstart. Where the bucket lives, how many resources go in one state, which provider gets used, whether anything is gitignored. None of them feel like architecture at the time, and all of them are.
The one that deserves a second look is the smallest. The state file is treated everywhere as plumbing — a build artefact, an implementation detail of a tool. Google's documentation says plainly that it stores secret values in plaintext and that only the build system and the most privileged administrators should be able to read it. Those two sentences describe a credential store, and the bucket holding it is almost always created before anyone in the organisation has written a policy about credential stores. It is worth going back and checking who can read yours.
#26 hands the whole problem to Google: Infrastructure Manager, the managed Terraform service — what it takes over, what it does not, and which of this post's decisions it makes for you.
Comments