Which files produced by a Terraform run must never be committed to a Git repository, and why is a saved plan file created with terraform plan -out=tfplan as sensitive as the state file itself?
answer
- commit what you wrote, not what Terraform produced
- the lock file is the exception
- backup file counts as state
- a plan is not a diff summary
- binary artifact, still plain-text secrets
basics
~20 sNever commit terraform.tfstate, terraform.tfstate.backup, the .terraform directory, saved plan files, crash logs, or tfvars files holding secrets. A saved plan embeds the prior state plus planned attribute values, so it carries the same plain-text credentials state does.
solid answer
~40 sThe rule of thumb is: commit what you wrote, never what Terraform produced about your real infrastructure. That means `terraform.tfstate` and `terraform.tfstate.backup` are out, the `.terraform` working directory is out, `crash.log` is out, and any `*.tfvars` file carrying real credentials is out. The one Terraform-generated file that *should* be committed is `.terraform.lock.hcl`, because it pins provider versions and checksums for everyone. A saved plan file surprises people: `terraform plan -out=tfplan` is not a diff summary, it is a binary archive containing the prior state, the configuration and every planned attribute value — so a database password inside state is inside the plan file too, unencrypted. Treat it exactly like state: never in Git, and in CI keep it as a short-lived, access-controlled artifact rather than something the whole org can download.
go deeper
Be able to list the ignored set from memory — tfstate, tfstate.backup, .terraform, plan files, crash.log, tfvars with real values — and name the lock file as the one generated file you do commit.
Explain why each entry is on the list: state and plan files carry plain-text credentials and real resource identifiers, .terraform holds platform-specific binaries, and the lock file exists precisely so provider resolution is reproducible.
Extend the rule to the pipeline: plan artifacts, build logs and job caches are the same exposure as a committed file. Be ready to say that remediation of a leak starts with rotation, not with history rewriting.
Set the guardrail so this cannot recur — a repository template with the gitignore in place, pre-commit and CI secret scanning, artifact retention policy, and a documented rotation runbook for when something slips through anyway.
## The two categories Everything in a Terraform working directory falls into one of two buckets. **Source — commit it.** `*.tf` files, `*.tf.json`, example variable files with no real values (`terraform.tfvars.example`), the README, and `.terraform.lock.hcl`. The lock file is generated by Terraform but is unambiguously source: it pins provider versions and their checksums so every developer and every CI runner resolves the same providers. **Output about real infrastructure — never commit it.** - `terraform.tfstate` — plain-text JSON containing real resource identifiers and every cached attribute, including secrets; - `terraform.tfstate.backup` — the previous state, written on each apply, with the same contents; - the `.terraform/` directory — downloaded providers and modules (large, platform-specific) plus a cached record of your backend configuration; - saved plan files (`tfplan`, or whatever you passed to `-out`); - `crash.log` — a Terraform crash dump, which can contain in-memory values; - `*.tfvars` / `*.auto.tfvars` when they hold real credentials — the file is source-shaped, but a committed one with a live password is the same incident as committed state. HashiCorp's own gitignore template for Terraform covers exactly this set, and it explicitly tells you *not* to ignore `.terraform.lock.hcl`. ```gitignore *.tfstate *.tfstate.* .terraform/ crash.log *.tfvars !*.tfvars.example tfplan ``` ## Why a saved plan is not "just a diff" The human-readable output you see on screen is a rendering. The file written by `-out` is a binary archive built for one purpose: so that `terraform apply tfplan` can execute *exactly* the actions that were previewed, with no re-evaluation. To do that it must carry: - the prior state Terraform refreshed at plan time — with all cached attributes; - the configuration and the resolved variable values used; - the planned value of every attribute of every resource being created or changed. That is a superset of the sensitive material in state, and none of it is encrypted. `terraform show -json tfplan` will print it back out. The redaction you saw in the terminal — `(sensitive value)` — was applied when rendering, not when writing. The practical consequence in CI: a common pipeline plans on the pull request, stores the plan as a build artifact, and applies it after merge. That artifact is a credential-bearing file. It needs the same treatment as state — restricted download permissions, a short retention period, and never a public build log or an artifact store the whole organisation can browse. ## When the rule is already broken If state or a plan file has already been committed, the priority is not the Git history — it is the credential. Rotate anything the file contained, because you cannot know who cloned the repository, and history rewriting does not reach forks, mirrors, CI caches or local clones. Clean the history afterwards if you like, but treat the secret as compromised from the moment it was pushed. ## What to say in the interview Give the list, then give the reason behind the list: committed state is not just untidy, it is a credential disclosure *and* a correctness hazard — two people applying from two Git checkouts have no locking and no single source of truth, which is the reason remote backends exist. Then add the plan-file point unprompted; it is the part most candidates miss, and it shows you have thought about the CI pipeline rather than just the local directory.
- Which Terraform-generated file should be committed, and why?`.terraform.lock.hcl`. It records the exact provider versions selected and their checksums, so every developer and every CI runner installs identical providers rather than silently picking up a new release. It is generated, but it is source: committing it is what makes provider resolution reproducible and auditable in review.
- Someone already pushed a terraform.tfstate containing a live password. What is the first thing you do?Rotate the credential. The repository may have been cloned, forked, mirrored or cached by CI before anyone noticed, so the value must be treated as compromised the moment it was pushed. Rewriting history is cleanup, not remediation, and it never reaches copies that already left your server.
- Your pipeline saves a plan file as a build artifact between the plan and apply jobs. What must you get right?Treat the artifact as a secret. Restrict who can download it, keep retention short, and never echo `terraform show -json` of it into a build log. Also bound its lifetime for correctness: a plan applied long after it was produced can be stale, so pair the short retention with a rule that a plan is only applied while it is fresh.
saying these in an interview costs you the question
- Says state is fine to commit if the repository is private
- Thinks a saved plan file only contains a summary of changes
- Adds .terraform.lock.hcl to .gitignore along with the state files
- Forgets terraform.tfstate.backup and crash.log
- Believes rewriting Git history un-leaks a committed credential