Workspaces, Lifecycle Rules, and Dependencies
Workspaces, Lifecycle Rules, and Dependencies
These are the controls that change what a plan is allowed to do: which state you are in, whether a resource may be destroyed, and which edges the graph waits for. They are also easy to use as a substitute for a layout you should have drawn in chapter 14.
Learning goals
- Explain a workspace as a separate state, not a separate configuration
- Decide when a workspace is the wrong environment boundary
- Set
create_before_destroy,prevent_destroy, andignore_changesfor a reason - Read a dependency graph and force replacement without editing the cloud by hand
Workspaces: Multiple Environments from One Configuration
A workspace is a named state inside one backend, selected with tofu workspace select. The configuration does not change when you switch. terraform.workspace (the name of the value, kept for compatibility) is a string you can interpolate, usually into names.
resource "random_pet" "service" {
length = 2
prefix = terraform.workspace
}
default exists even when you have never created another workspace. tofu workspace new staging makes a second state. Apply in staging does not update default. That isolation is the feature.
The CLI remembers the selected workspace in local files. A pipeline must select the workspace explicitly (tofu workspace select production) and fail if the select fails. An apply that runs in default because the select was skipped is an apply into the wrong state.
When Workspaces Help (and When They Hurt): Practical Guidance
Workspaces help when the environments are the same shape, the same permissions boundary, and the same blast radius: a developer sandbox per person, or a short-lived test state. They hurt when production and development differ in account, credentials, or who is allowed to apply.
Production state in the same backend configuration as development, separated only by a workspace name, means a missed workspace select can plan against production with development credentials or the reverse. Separate root modules, separate backend keys, and separate pipelines make that mistake a file you did not mean to enter rather than a flag you forgot.
Do not use the workspace name as the only difference in a large conditional (terraform.workspace == "production" ? big : small) that changes counts and providers. That configuration cannot be read without knowing which state it will meet. Two directories that call the same child module with different variables are longer and clearer.
If you already have workspaces and they are small, keep them until the permission boundary appears. Do not migrate in the middle of an incident.
Lifecycle Arguments: create_before_destroy, prevent_destroy, ignore_changes
lifecycle is a meta-argument on a resource.
create_before_destroy = true creates the replacement before deleting the old object. Use it when the name can differ (so both can exist) and something else will retarget, such as a listener that follows the new instance. It fails when the identity must be unique, such as a fixed bucket name. The plan will tell you the create cannot proceed while the old object holds the name.
prevent_destroy = true makes any plan that would destroy the resource an error. Use it on the state bucket’s own configuration if that configuration lives in OpenTofu at all, and on data you cannot recreate. It also makes refactors louder, which is what you want. Removing the flag is a reviewed change, not a quick comment-out during an apply.
ignore_changes tells the plan to leave listed arguments alone even if the configuration and the world differ.
resource "random_pet" "service" {
length = 2
lifecycle {
ignore_changes = [separator]
}
}
Ignore a field when something outside OpenTofu is the writer of that field and you have accepted the split: an autoscaler setting capacity, a security tool setting a tag. Ignoring all with ignore_changes = all hides every drift on that resource. That is rarely what you meant. Ignoring an argument you set yourself hides your own edits. Take the argument out of the configuration if OpenTofu should not own it.
replace_triggered_by forces replacement when another value changes. Use it when a resource does not see an argument change that should have rebuilt it. Prefer fixing the argument over a trigger when the provider supports the change.
Dependency Management: Explicit depends_on vs. Implicit References
A reference is a dependency. If module.record takes module.name.name, the record waits for the name. You do not add depends_on for that edge. The graph already has it.
depends_on is for edges the configuration cannot see: a data source that reads an object created in the same apply, or a provisioner-shaped side effect you have not yet removed. Every explicit edge is a serialisation. A module-level depends_on waits for the entire module, which is coarser than waiting for one output and can slow the plan’s apply for no gain.
Do not depends_on a resource to paper over a missing attribute reference. Pass the attribute. The next reader can then see why the order exists.
Cycles are errors. A cycle means two objects each need the other’s id. Break it by creating one of them with a name you already know, or by splitting the states. A cycle is a design signal, not a flag to relax.
The Dependency Graph: Visualizing and Debugging Execution Order
tofu graph prints a DOT graph of the addresses and their edges. Render it when you do not believe the order an apply is taking. Most of the time the plan’s explanation of a replacement is enough and the full graph is noise.
What you are looking for:
- An edge you did not intend, usually from a reference used only in a tag
- A missing edge, usually a data source that reads too early
- A module that everything depends on because its outputs are a grab-bag
tofu apply -target=random_pet.service applies only that node and what it requires. Targeting is a recovery tool. An apply that always targets has left the declarative model, and the untargeted objects drift from the configuration without anyone noticing. Follow a targeted apply with a full plan the same day.
Tainting Resources: Forcing Recreation When Needed
Tainting marks an object so the next plan replaces it, even though the configuration did not change. The modern form is:
tofu apply -replace=random_pet.service-replace is the flag to use. The older tofu taint still exists and is easier to forget in state. Replacement runs the destroy and create according to create_before_destroy and the graph. For the pet resource that only changes a name. For a database it deletes data. Read the plan.
Replace when the object is wrong in a way the configuration cannot express: a virtual machine whose user-data ran once and failed, a secret that was issued and must be reissued. Do not replace as a substitute for understanding a drift. If the configuration is wrong, fix the configuration. If the world is wrong and the configuration is right, replace.
Summary
- A workspace is another state for the same configuration. Select it explicitly
- Separate roots when accounts, credentials, or blast radius differ
prevent_destroyandcreate_before_destroyare promises about replacement.ignore_changesis a list, notall- References build the graph.
depends_onis for edges a reference cannot see -replaceforces recreation.-targetis a recovery hatch followed by a full plan