Migrating From count to for_each Without Destroying Anything
Goal
You confirm the difference between count and for_each through state addresses, and learn the procedure for migrating an already applied resource to for_each without destruction.
Why it matters
count and for_each look like a choice of syntax, but in fact they are a decision about what to use as each instance's identifier. count uses order and for_each uses names as the identifier. If you delete an item from the middle of a list, the difference shows — with count, the later ones are pulled forward one slot and even resources that should have stayed are recreated, while with for_each, only that key disappears. On a real cloud, this difference shows up as "I deleted one line of configuration and two servers were recreated." When you need to change the address of an already applied resource, use a moved block. moved only moves the state and does not touch the real thing, and the refactoring history remains in the code, so others follow automatically just by running plan. There is one criterion for judging whether the migration went right — the plan must have no destruction at all.
Steps
- In
/root/tf/foreach/main.tf, declare thelocal_fileresourcenodewithcount = 3, runtofu init, and apply. Then, before moving addresses, save the result oftofu state listto/root/tf/foreach/out/count-state.txt. This file must contain the three lineslocal_file.node[0],local_file.node[1], andlocal_file.node[2](it must not contain string-key forms), and it serves as the pre-migration snapshot for step 7. - Make the file name of
node/root/tf/foreach/out/node-${count.index}.txtand its contentsnode-${count.index}. Do not write the name by hand three times; you must build it from the repetition index. There must be exactly 3node-*.txtfiles underout, and the first lines of the files must benode-0,node-1, andnode-2. - Make the
local_fileresourcezonewithfor_each = toset(["kr", "us", "eu"])and have three files/root/tf/foreach/out/zone-<키>.txtcreated (where the placeholder is the key). The keys in the state must beeu,kr, andus. - Iterate the
local_fileresourceserviceover a map. The keys areapi,web, andworker, and each value holds a port (the port ofwebmust be8080). Inside/root/tf/foreach/out/service-web.txt,name=webandport=8080must each be on its own line. - With a
forexpression, define an outputservice_portsthat has the service name as the key and the port as the value, and save the result oftofu output -jsonto/root/tf/foreach/out/outputs.json. There must be 3 entries, andwebmust be8080. - Create
variable "enable_debug"(bool, default false) and usecount = var.enable_debug ? 1 : 0on thelocal_fileresourcedebug. The file path is/root/tf/foreach/out/debug.txtand the contents are free (the reason you may keep usingcountis that a switch has at most one instance, so there is no chance of the index shifting). Save the output of the plan with the switch turned on (tofu plan -var enable_debug=true) to/root/tf/foreach/out/toggle.txt(it must show1 to add), but do not apply it. In the final state there must be 0debuginstances and/root/tf/foreach/out/debug.txtmust not exist either. - Change
nodetofor_each = { n0 = 0, n1 = 1, n2 = 2 }. The map's keys are for the new addresses and the values are the old indexes, so you must build the file name and contents fromeach.value, noteach.key, and keepnode-0.txt,node-1.txt,node-2.txtand their contents exactly the same as in steps 1–2 (if you change the names, it becomes recreation rather than migration and destruction shows up in the plan). And connect the old addresses and new addresses with 3movedblocks, such as movinglocal_file.node[0]tolocal_file.node["n0"]. After you apply, if you save thetofu planoutput to/root/tf/foreach/out/after-plan.txt, it must beNo changeswith no destruction at all. - Create
/root/tf/foreach/out/addresses.json. Put the addresses fromtofu state listas an array under theaddresseskey (node 3 + zone 3 + service 3 = 9 or more), and there must be no address with a numeric index. Write the number of numeric-index addresses in thenumeric_index_countkey, which is0if the migration is finished.
Notes
- The state file for this lab is
/root/tf/foreach/terraform.tfstate, and the outputs made with count are/root/tf/foreach/out/node-0.txt,/root/tf/foreach/out/node-1.txt, and/root/tf/foreach/out/node-2.txt. These file names must stay the same after the migration. tofu state listspits out addresses one per line. If you read it whole withjq -R -sand fold it into an array, step 8 is easy.count-state.txtin step 1 andaddresses.jsonin step 8 are before and after snapshots of the same command. In a real migration too, the first step is to leave the list of addresses and a copy of the state from before the move. Without a record, there is no way to prove "no resource was missed."- It is conventional to leave a
movedblock in the code even after applying it once. That is because the same migration must happen in other people's state too. - The
toggle.txtin step 6 must be taken after all other changes are applied so that the plan picks up only the one switch. - Common mistake 1: changing the file name to
node-${each.key}.txtin step 7. If the name changes, it becomes recreation rather than migration and destruction shows up in the plan. - Common mistake 2: passing a list to
for_eachas is. You must convert it to a set withtoset(). - Common mistake 3: taking
count-state.txtafter finishing the migration. That file is meaningful only if it is a record from before the move.
Create three with count and record the addresses before moving
In /root/tf/foreach/main.tf, declare the local_file resource node with count = 3, run tofu init, and apply. Then, before moving addresses, save the result of tofu state list to /root/tf/foreach/out/count-state.txt. This file must contain the three lines local_file.node[0], local_file.node[1], and local_file.node[2] (it must not contain string-key forms), and it serves as the pre-migration snapshot for step 7.
If you put count = 3 in the resource block, three instances are created. You will move the addresses later, so keep the current address list in a file — without a record from before the move, you cannot prove what moved and how.
Distinguish instances with count.index
Make the file name of node /root/tf/foreach/out/node-${count.index}.txt and its contents node-${count.index}. Do not write the name by hand three times; you must build it from the repetition index. There must be exactly 3 node-*.txt files under out, and the first lines of the files must be node-0, node-1, and node-2.
Each instance needs a different file name, so insert the index into the name. Distinguish the contents in the same way.
Use for_each over a set
Make the local_file resource zone with for_each = toset(["kr", "us", "eu"]) and have three files /root/tf/foreach/out/zone-<키>.txt created (where the placeholder is the key). The keys in the state must be eu, kr, and us.
You cannot pass a list directly to for_each. You must convert it to a set, and then the key becomes the element value itself.
for_each over a map and each.value
Iterate the local_file resource service over a map. The keys are api, web, and worker, and each value holds a port (the port of web must be 8080). Inside /root/tf/foreach/out/service-web.txt, name=web and port=8080 must each be on its own line.
When you pass a map, each.key is the map's key and each.value is its value. If the value is an object, you can dig into its fields.
Build an output map with a for expression
With a for expression, define an output service_ports that has the service name as the key and the port as the value, and save the result of tofu output -json to /root/tf/foreach/out/outputs.json. There must be 3 entries, and web must be 8080.
for_each is a meta-argument that multiplies resources, and for is an expression that turns a value into another shape. Use the form { for k, v in ... : k => ... }.
Make a feature switch with a conditional count
Create variable "enable_debug" (bool, default false) and use count = var.enable_debug ? 1 : 0 on the local_file resource debug. The file path is /root/tf/foreach/out/debug.txt and the contents are free (the reason you may keep using count is that a switch has at most one instance, so there is no chance of the index shifting). Save the output of the plan with the switch turned on (tofu plan -var enable_debug=true) to /root/tf/foreach/out/toggle.txt (it must show 1 to add), but do not apply it. In the final state there must be 0 debug instances and /root/tf/foreach/out/debug.txt must not exist either.
있으면 1, 없으면 0 is the on/off idiom (1 if present, 0 if absent). Check the turned-on state only with a plan and do not actually apply it.
Migrate count to for_each with a moved block
Change node to for_each = { n0 = 0, n1 = 1, n2 = 2 }. The map's keys are for the new addresses and the values are the old indexes, so you must build the file name and contents from each.value, not each.key, and keep node-0.txt, node-1.txt, node-2.txt and their contents exactly the same as in steps 1–2 (if you change the names, it becomes recreation rather than migration and destruction shows up in the plan). And connect the old addresses and new addresses with 3 moved blocks, such as moving local_file.node[0] to local_file.node["n0"]. After you apply, if you save the tofu plan output to /root/tf/foreach/out/after-plan.txt, it must be No changes with no destruction at all.
When an address changes, the tool reads it as a destroy and a create. Connect the old and new addresses with moved, and keep the file names and contents unchanged so that there is no recreation.
Create the full address list and the migration completion report
Create /root/tf/foreach/out/addresses.json. Put the addresses from tofu state list as an array under the addresses key (node 3 + zone 3 + service 3 = 9 or more), and there must be no address with a numeric index. Write the number of numeric-index addresses in the numeric_index_count key, which is 0 if the migration is finished.
Turn the state list into a JSON array and put it in. Count for yourself whether any numeric-index address remains.