skip to content

After `terraform apply -refresh-only` records a colleague's console change into Terraform state, what does the very next plain `terraform plan` propose, and why?

level: seniorimportance: should knowfreq 38%

answer

  1. state was fixed, configuration was not
  2. plan diffs config against state
  3. the code is the source of truth, always
  4. empty plan is the finish line
  5. a standing diff trains people to stop reading plans

basics

~20 s

It proposes changing the resource back to whatever the configuration says. Refresh-only updates state, never the .tf files, so the plan now compares an accurate state against an unchanged config and sees a difference it intends to correct.

solid answer

~40 s

Refresh-only fixed the wrong half. State is now accurate — it records the console value — but the configuration still says what it always said, and a plain plan diffs configuration against state. So the plan proposes to move the resource back to the configured value, and an apply would undo your colleague's change. That asymmetry is the point people miss: Terraform will happily record reality, but it never authors your code, so accepting drift into state settles nothing on its own. If the team wants to keep the change, someone edits the HCL to match what the console did and the plan goes quiet. If the change was wrong, you already have the fix — just apply the plan. Either way the decision is a code review, not a state operation.

go deeper

for a junior

Remember that Terraform never writes your .tf files. If you want to keep a change someone made outside Terraform, a human has to put that value into the code.

for a middle

Explain the direction of each write: apply changes reality and state, refresh-only changes state alone, and a plan diffs configuration against state — which is why the console change reappears as a proposed revert.

for a senior

Show that you drive to an empty plan. Read the refresh-only diff for the exact value, decide adopt-or-revert with the owning team, and never leave a module with a permanent standing diff.

for a principal

Own why adoption goes through code review rather than state: the repository stays the reviewed source of truth, and every emergency console change ends up with an author, a reviewer and a rationale in the history.

## The asymmetry Terraform holds three pictures of the world — configuration, state, and reality — and it can only write two of them. It writes reality (on apply) and it writes state (on apply, and on a refresh-only apply). It never writes your configuration. Every reconciliation story in Terraform comes back to that. So after `terraform apply -refresh-only` accepts a console change: - **reality** = the console value (unchanged by refresh-only) - **state** = the console value (just updated) - **config** = the old value (nobody edited it) A plain `terraform plan` refreshes, then diffs configuration against state. Two of the three now agree with each other and disagree with the code, so the plan proposes exactly one thing: bring the resource back to the configured value. An apply at that moment reverts your colleague's 3am change. ## Why that is the correct behaviour It looks unhelpful until you say it in the other direction. If Terraform silently adopted whatever it found in the world, the repository would stop being the source of truth: an attacker or a careless click could rewrite your infrastructure's definition just by touching the console, and the next reviewer would never see it. Terraform's contract is that the code decides and the code is reviewed. Refresh only ever changes what Terraform *believes exists*, never what it has been *told to build*. ## Making the plan go quiet There is only one way to keep an out-of-band change without leaving a permanent diff: change the code. Edit the HCL until the configured value matches what the console set, open a pull request, and let the reviewer see the value the incident actually needed. Then the plan is empty, and it is empty for the right reason — code, state and reality genuinely agree. The verification loop is simple, and it is what an interviewer wants to hear: ```bash terraform apply -refresh-only # state now matches reality # edit the .tf file to match the new value terraform plan # target: "No changes." ``` If the plan is not empty, you have not finished reconciling — you have merely made the diff smaller. Chasing that empty plan is the discipline; a repository whose plan is permanently non-empty trains everyone to stop reading plan output, which is how the next real change slips through unnoticed. ## Why bother with the refresh-only step at all If a normal apply would revert the change anyway, why record it first? Three reasons, all practical: 1. **You see the exact value.** The refresh-only plan prints attribute-level before/after pairs, so you know precisely what to write into the code — no guessing from a console screenshot. 2. **You separate the decisions.** Recording the fact is safe and instant; deciding whether to keep it is a conversation. Doing them in one step means somebody decides under pressure. 3. **The follow-up diff is honest.** With state accurate, your next plan shows only the config-versus-reality question and nothing else, instead of mixing drift with whatever else state was stale about. ## The trap: doing nothing The failure mode is not reverting and it is not adopting — it is leaving state accepted and the code untouched. Now every plan anyone runs on that module carries a change nobody intends to apply, and the first person in a hurry approves it along with their own. Accepting drift into state without a follow-up code change converts a one-time incident into a permanently noisy plan. ## What to say out loud Refresh-only updates state; the next plan diffs config against state; therefore the plan proposes to undo the console change. To keep it, edit the configuration until the plan is empty. To reject it, apply the plan. What you must not do is stop halfway and leave a standing diff.

  • What if you skip the refresh-only step and just run a normal apply after the console change?
    Much the same outcome, faster and blinder: the plan's own refresh notices the drift, the diff proposes reverting it, and approving reverts your colleague's change. You lose the chance to see the exact observed value in isolation and to decide deliberately, which matters most when the change was made during an incident and might still be load-bearing.
  • How do you verify the reconciliation is actually finished?
    A plain `terraform plan` reports no changes. Until it does, config, state and reality do not all agree, and the remaining diff is something someone will eventually approve by accident. Treat a clean plan as the definition of done for any drift ticket.
  • Whose call is it whether to adopt the console change into the code?
    The module owners', through normal code review — the pull request that edits the HCL is the decision record. That is the practical value of routing adoption through configuration instead of state: the change gets an author, a reviewer and a diff, exactly like any other production change.

Refresh-only is updating your inventory sheet after someone moved the furniture; the floor plan on the wall still shows the old layout, and the next crew works from the floor plan.

saying these in an interview costs you the question

  • Thinks refresh-only also updates the .tf configuration
  • Expects the next plan to show no changes after accepting drift into state
  • Leaves the standing diff in place and tells the team to ignore it
  • Believes state is the source of truth over configuration
  • Hand-edits the state file instead of editing the code

context