fmt and validate
terraform fmt and terraform validate are the two cheapest checks Terraform ships, and they belong in CI as the first gate before anything slower runs. fmt rewrites your HCL into one canonical style so diffs stop arguing about spacing. validate checks that the configuration is internally consistent — syntax parses, references resolve, argument types match — and it does this without calling a single Google Cloud API.
The catch that trips up everyone once is what validate does not do. A config can pass validate cleanly and still fail at apply because it names a bucket someone already owns, requests an API that is disabled, or sets an IAM binding the caller cannot grant. "Validates" means internally consistent, not "the apply will work." Keep that line sharp and these two commands earn their place at the front of every pipeline.
What fmt Does
terraform fmt rewrites every .tf file in a directory to canonical indentation, alignment, and spacing — two-space indents, aligned = signs within a block, consistent blank lines. It changes formatting only, never meaning. The value is that style stops being a thing humans argue about in review: there is exactly one correct layout, and a machine produces it.
In CI the relevant form is terraform fmt -check -recursive. The -check flag makes it report rather than rewrite, exiting non-zero when any file is unformatted, and -recursive walks subdirectories so modules are covered too. Wire that as a step that fails the build, and a misformatted file never reaches a reviewer.
# locally: rewrite files in place terraform fmt -recursive # in CI: fail if anything is unformatted, change nothing terraform fmt -check -recursive
The first form is what you run on save or before a commit; the second is what the pipeline runs. They share an opinion about what "formatted" means, so an engineer who runs fmt locally will always pass the -check gate.
What validate Checks
terraform validate confirms the configuration is internally consistent. It checks that the HCL parses, that every reference resolves to something declared — that google_storage_bucket.raw.name points at a bucket resource that actually exists in the config — and that argument types and required fields are satisfied against the provider's schema. A required argument left out, a string where a number belongs, a reference to a resource you deleted: all caught here.
All of that happens against the provider schema, not against Google Cloud. validate reads no state and makes no API calls. It is checking the shape of your config, not the state of your project — which is exactly why it is fast and safe to run anywhere.
What validate Does NOT Check
This is the distinction worth internalizing. validate will pass a config that names a bucket someone else already owns, that requests a resource in compute.googleapis.com when that API is disabled on the project, or that sets an IAM binding the running identity has no permission to grant. None of those are inconsistencies in your config — they are facts about the real world that validate never consults.
Those failures surface only at plan or apply, when Terraform actually talks to GCP. So validate answers "is this config well-formed?" and plan answers "will this config work against my project right now?" Treating a green validate as a promise that apply will succeed is the single most common misreading of the command.
validate Needs init, Not Credentials
validate needs the provider plugins installed, because it checks your arguments against the provider's schema — and that schema ships inside the google provider. So terraform init must run first to download providers. But it needs no credentials and reads no state, because it never calls an API.
That combination is what makes validate ideal early in CI: run init with the provider cache, then validate offline, with no service account key or workload identity in scope. The credentialed steps come later, only for configs that already parse cleanly.
The Cheap-First Gate
Order a CI pipeline cheap-to-expensive so the fast checks catch the dumb errors before the slow ones spend money: fmt -check first, then validate, then tflint and security scanners, then plan against real GCP. The first two are offline and uncredentialed and finish in seconds. plan needs credentials, reads state, and calls APIs — so it runs last, only on configs that already passed everything cheaper.
The payoff is that a missing comma or an unresolved reference fails the build in seconds, not after a minute of provider negotiation and state refresh. A pipeline that runs plan first wastes credentialed time discovering errors a free offline check would have caught instantly.
# 1. offline, no credentials, seconds terraform fmt -check -recursive terraform init -backend=false terraform validate # 2. offline scanners (next topic) tflint --recursive # 3. credentialed, calls GCP, runs last terraform plan -out=plan.out
Note init -backend=false in the offline stage: it installs providers so validate has schemas, without configuring the GCS backend or touching remote state. The real init with the backend happens in the credentialed stage before plan.
validate — checks the config is internally consistent: parses, references resolve, types and required fields satisfied. Offline, no credentials, no state. Answers "is this well-formed?" Run it as the first correctness gate.
plan — checks the config against your actual project: reads state, calls GCP APIs, surfaces duplicate names, disabled APIs, and missing permissions. Needs credentials. Answers "will this work right now?" Run it last, after every cheaper check passes.
- Believing a passing
validatemeans the apply will succeed — it says nothing about a duplicate bucket name, a disabledcompute.googleapis.com, or a missing IAM permission, all of which only surface at plan or apply against real GCP. - Running
validatebeforeinitand getting a confusing error about uninstalled providers — validate needs the provider schemas to check against, so init must come first. - Skipping
fmt -check -recursivein CI and letting formatting churn pile into feature diffs, so a reviewer cannot tell a real change from a re-indent. - Treating
fmtas cosmetic and only running it locally for some engineers — once half the team formats and half does not, every PR carries spurious whitespace noise. - Putting
planfirst in the pipeline and burning credentialed minutes on configs a free offlinevalidatewould have rejected in seconds. - Forgetting
-recursiveonfmt -check, so the root module is checked but the modules under it drift out of canonical style unnoticed.
- Run
terraform fmt -check -recursiveas a CI step that fails on any unformatted file, and enable fmt-on-save in the editor so it never reaches review. - Run
terraform validateafterinitas the first offline correctness gate, before any credentialed step. - Order the pipeline cheap-to-expensive:
fmtandvalidatewith no credentials, then scanners, thenplanwith credentials, so failures surface fast and cheap. - Use
init -backend=falsein the offline stage to install providers forvalidatewithout configuring the GCS backend or touching remote state. - Treat
validateas a config-consistency check and reach forplanfor real-world feasibility against GCP — never conflate the two.
plan than to validate
tflint the next rung — overlapping linting beyond consistency
Knowledge Check
A config passes terraform validate. Which problem could still break the apply?
- The bucket name is globally unique and already owned by another project
- A required argument was omitted from a resource block in the configuration
- A resource attribute reference points to another resource that does not exist anywhere in the config
- A bare string value was given for an argument where the provider schema expects a number
Why does validate require terraform init but not credentials?
- It checks arguments against the provider schema, which ships inside the provider, but it makes no API calls
- It reads the current remote state from the GCS backend that terraform init configures during setup
- It needs credentials only to fetch the provider schema once, then caches them locally after init
- init compiles the whole configuration into a single self-contained binary that validate then executes against the provider schema
What is the correct cheap-to-expensive ordering of CI checks?
fmt -check→validate→ scanners →planplan→validate→fmt -check→ scannersvalidate→plan→fmt -check→ scanners- scanners →
plan→fmt -check→validate
Why does fmt belong in CI as a hard gate rather than a local nicety?
- When only some engineers format locally, every PR carries spurious whitespace noise that hides real changes
- fmt rewrites resource argument values, so unformatted code can silently change the infrastructure that gets provisioned
- fmt validates that every cross-resource reference actually resolves, which plain validate cannot do
- fmt is the only CI check that can detect a globally duplicate bucket name before the apply runs
You got correct