The removed user's file stayed behind only on the script side
Goal
Feed the same roster to an imperative script and to OpenTofu, compare what each approach does when the roster shrinks, and confirm by hand the ownership scope, the addressing scheme (count versus for_each), and moving with moved.
Why it matters
An imperative script knows only "what to do now," so to clean up what dropped off the roster, it has to remember separately what it created in the past. A declarative tool keeps that memory in the state file, so it computes what disappeared from the code as a deletion. That does not mean it knows everything, though — the tool knows only what it created, and a file someone put into the same directory by hand does not appear in the plan. And if you address by position number, a single line at the front of the list causes the accident of every later resource being replaced, so address by key and, when moving, tell the state with moved.
Steps
- In
/root/iac-conv/users.txt, writealice,bob, andcarol, one per line./root/iac-conv/imperative.sh <명단파일> <출력디렉터리>(the placeholders are the roster file and the output directory), for each name in the roster, writes to<출력>/<이름>.txtthe lineuser <이름>and appends the name as one line to<출력>/registry.log. Run this script twice with/root/iac-conv/users.txtand/root/iac-conv/imp. - In
/root/iac-conv/main.tf, read the sameusers.txt(excluding blank lines) and declarelocal_file.user["<이름>"]with for_each for each name. The file is${path.module}/decl/<이름>.txt, and its content is the single lineuser <이름>. Run init and apply. The grader checks whether the keys in the state are exactly the same as the names in users.txt, and whether the plan is clean. - From
users.txt, removecarol. Run the imperative script once more with the same arguments, and on the declarative side, save the plan withtofu plan -out=/root/iac-conv/remove.tfplanand then apply that plan. The grader checks whether the saved plan contains only the deletion of carol, and whetherdecl/carol.txtis gone whileimp/carol.txtremains. - Someone created
/root/iac-conv/decl/mallory.txtby hand (create it yourself, with any content). Confirm thattofu plan -detailed-exitcodeis still 0. Then create/root/iac-conv/orphans.sh <관리디렉터리> <작업디렉터리>(the placeholders are the managed directory and the working directory) so that it prints, one per line, the names of files that are in the managed directory but unknown to the state, and save the result fordeclto/root/iac-conv/orphans.txt. - In
/root/iac-conv/count-demo/main.tf, put anamesvariable (default["a", "b", "c"]) and alocal_file.ncreated with count. The files areout/<인덱스>.txt(index), and the content is that name on one line. After applying, remove"a"from the default and only save, withtofu plan -out=/root/iac-conv/count-demo/shift.tfplan(do not apply). In/root/iac-conv/count-demo/shift.txt, write<주소> <update|replace|delete|create>(address and action) for each resource that changes, and finally put the default back to["a", "b", "c"]. - In
/root/iac-conv/foreach-demo/main.tf, put the samenamesvariable and thefor_each = toset(var.names)version oflocal_file.n. The files areout/<이름>.txt(name), and the content is the name on one line. After applying, remove"a"from the default and only save, withtofu plan -out=/root/iac-conv/foreach-demo/drop.tfplan. Write to/root/iac-conv/foreach-demo/drop.txtin the same format as step 5, and put the default back to["a", "b", "c"]. - In
/root/iac-conv/count-demo, changelocal_file.nto a for_each keyed by name, but leave the file names (out/0.txt,out/1.txt,out/2.txt) and the contents as they are, and using threemovedblocks, tell the staten[0]→n["a"],n[1]→n["b"],n[2]→n["c"]. Confirm that the plan has no creations, deletions, or replacements at all, and then apply. The grader checks that the state addresses have changed to name keys and that the plan is clean. - Create
/root/iac-conv/converge.sh <작업디렉터리> <관리디렉터리>(the placeholders are the working directory and the managed directory). After apply, if the secondplan -detailed-exitcodeis not 0, it finishes with a line starting withnot-convergedand 2; if there is a stray file found by orphans.sh, with a line starting withstray(including the file name) and 3; if both are fine, withconvergedand 0; and if apply or plan fails, with a line starting witherrorand 1. The grader checks with a copy of/root/iac-conv(3, because mallory.txt is there) and temporary directories it builds itself. You do not need to run it on the original.
Notes
- The Pod has OpenTofu 1.9.0 and a local provider mirror, so init works without internet.
- Reading the actions from a plan:
tofu show -json <계획파일> | jq '.resource_changes[] | {address, actions: .change.actions}'(the placeholder is the plan file) - Common mistake: changing even the file name expression while moving with moved, so that an extra replacement follows the move.
- Common mistake: going as far as apply in a step that said only to make a plan, which changes the starting point of the next step.
- for_each · Refactoring(moved) · Resource Behavior · tofu show
Run the imperative script twice
In /root/iac-conv/users.txt, write alice, bob, and carol, one per line. /root/iac-conv/imperative.sh <명단파일> <출력디렉터리> (the placeholders are the roster file and the output directory), for each name in the roster, writes to <출력>/<이름>.txt the line user <이름> and appends the name as one line to <출력>/registry.log. Run this script twice with /root/iac-conv/users.txt and /root/iac-conv/imp.
> overwrites and >> appends. The file contents are the same after running twice, but registry.log grows by the number of runs. This difference is what a non-idempotent imperative script looks like.
Write the same roster as a declaration
In /root/iac-conv/main.tf, read the same users.txt (excluding blank lines) and declare local_file.user["<이름>"] with for_each for each name. The file is ${path.module}/decl/<이름>.txt, and its content is the single line user <이름>. Run init and apply. The grader checks whether the keys in the state are exactly the same as the names in users.txt, and whether the plan is clean.
Read it with file() and build the set with split, compact, and toset. for_each accepts only a set or a map. Confirm with tofu state list that what is inside the brackets of the resource address is a name, not an index number.
Remove one line from the roster — only one side cleans up
From users.txt, remove carol. Run the imperative script once more with the same arguments, and on the declarative side, save the plan with tofu plan -out=/root/iac-conv/remove.tfplan and then apply that plan. The grader checks whether the saved plan contains only the deletion of carol, and whether decl/carol.txt is gone while imp/carol.txt remains.
A declarative tool calculates the difference between what is recorded in the state and the code, so a key that has disappeared from the code shows up as a deletion. An imperative script looks only at the current roster and does not know what it created in the past.
Even a declarative tool does not know files outside its ownership
Someone created /root/iac-conv/decl/mallory.txt by hand (create it yourself, with any content). Confirm that tofu plan -detailed-exitcode is still 0. Then create /root/iac-conv/orphans.sh <관리디렉터리> <작업디렉터리> (the placeholders are the managed directory and the working directory) so that it prints, one per line, the names of files that are in the managed directory but unknown to the state, and save the result for decl to /root/iac-conv/orphans.txt.
In the values of tofu show -json are the resources and attributes that the state knows. Compare the filename of local_file with the list of files in the directory. The scope a declarative tool converges is only the resources you declared.
We removed the front of the count list and everything after it changed
In /root/iac-conv/count-demo/main.tf, put a names variable (default ["a", "b", "c"]) and a local_file.n created with count. The files are out/<인덱스>.txt (index), and the content is that name on one line. After applying, remove "a" from the default and only save, with tofu plan -out=/root/iac-conv/count-demo/shift.tfplan (do not apply). In /root/iac-conv/count-demo/shift.txt, write <주소> <update|replace|delete|create> (address and action) for each resource that changes, and finally put the default back to ["a", "b", "c"].
The address of a count is a position number. If you take out the front, the later elements shift forward by one, and a different value goes into the same number. A local_file is replaced when its content changes.
With for_each, only what was removed goes away
In /root/iac-conv/foreach-demo/main.tf, put the same names variable and the for_each = toset(var.names) version of local_file.n. The files are out/<이름>.txt (name), and the content is the name on one line. After applying, remove "a" from the default and only save, with tofu plan -out=/root/iac-conv/foreach-demo/drop.tfplan. Write to /root/iac-conv/foreach-demo/drop.txt in the same format as step 5, and put the default back to ["a", "b", "c"].
The address of a for_each is the key. The keys "b" and "c" stay as they are regardless of their position in the list, so they do not appear in the plan.
Move from count to for_each without deleting anything
In /root/iac-conv/count-demo, change local_file.n to a for_each keyed by name, but leave the file names (out/0.txt, out/1.txt, out/2.txt) and the contents as they are, and using three moved blocks, tell the state n[0]→n["a"], n[1]→n["b"], n[2]→n["c"]. Confirm that the plan has no creations, deletions, or replacements at all, and then apply. The grader checks that the state addresses have changed to name keys and that the plan is clean.
A moved block is a promise to change only the address in the state. Only when the configuration at the new address is the same as the old object, down to its attributes, does just a move happen — if the file name changes, a replacement follows the move. To get the position number from the name, build a map with a for expression.
A convergence check: apply, then verify the second plan and stray files
Create /root/iac-conv/converge.sh <작업디렉터리> <관리디렉터리> (the placeholders are the working directory and the managed directory). After apply, if the second plan -detailed-exitcode is not 0, it finishes with a line starting with not-converged and 2; if there is a stray file found by orphans.sh, with a line starting with stray (including the file name) and 3; if both are fine, with converged and 0; and if apply or plan fails, with a line starting with error and 1. The grader checks with a copy of /root/iac-conv (3, because mallory.txt is there) and temporary directories it builds itself. You do not need to run it on the original.
Idempotency (the second plan is empty) and convergence (there is nothing in my area that I do not know about) are different questions. Call the orphans.sh from the earlier step as it is. A configuration whose second plan is not empty comes from a value that changes every time, such as timestamp().