Skip to content

Commit e00f080

Browse files
development docs: Planning Behaviors
For a while now we've had information equivalent to this in various internal documents that we've referred to when designing features such as config-driven refactoring, the "replace" planning option, and so forth. However, so far we've not put that information in any sort of durable public place that we can easily find and refer to when having design discussions on GitHub and similar. This is therefore an attempt to capture a summary of the three main design patterns we've identified for planning-related behaviors, with a few motivating examples of each one, in the hope that this will be a good reference and some helpful inspiration for future design work. It's intentionally not totally comprehensive of all planning behaviors both because that would duplicate the end-user-oriented documentation and because it would be burdensome to keep updating this document each time we add anything new which might fit into these categories. However, we might add a later feature to this document if it illustrates a new take or different perspective on one of these patterns.
1 parent 6c7db71 commit e00f080

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

‎docs/planning-behaviors.md‎

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Planning Behaviors
2+
3+
A key design tenet for Terraform is that any actions with externally-visible
4+
side-effects should be carried out via the standard process of creating a
5+
plan and then applying it. Any new features should typically fit within this
6+
model.
7+
8+
There are also some historical exceptions to this rule, which we hope to
9+
supplement with plan-and-apply-based equivalents over time.
10+
11+
This document describes the default planning behavior of Terraform in the
12+
absense of any special instructions, and also describes the three main
13+
design approaches we can choose from when modelling non-default behaviors that
14+
require additional information from outside of Terraform Core.
15+
16+
This document focuses primarily on actions relating to _resource instances_,
17+
because that is Terraform's main concern. However, these design principles can
18+
potentially generalize to other externally-visible objects, if we can describe
19+
their behaviors in a way comparable to the resource instance behaviors.
20+
21+
This is developer-oriented documentation rather than user-oriented
22+
documentation. See
23+
[the main Terraform documentation](https://www.terraform.io/docs) for
24+
information on existing planning behaviors and other behaviors as viewed from
25+
an end-user perspective.
26+
27+
## Default Planning Behavior
28+
29+
When given no explicit information to the contrary, Terraform Core will
30+
automatically propose taking the following actions in the appropriate
31+
situations:
32+
33+
- **Create**, if either of the following are true:
34+
- There is a `resource` block in the configuration that has no corresponding
35+
managed resource in the prior state.
36+
- There is a `resource` block in the configuration that is recorded in the
37+
prior state but whose `count` or `for_each` argument (or lack thereof)
38+
describes an instance key that is not tracked in the prior state.
39+
- **Delete**, if either of the following are true:
40+
- There is a managed resource tracked in the prior state which has no
41+
corresponding `resource` block in the configuration.
42+
- There is a managed resource tracked in the prior state which has a
43+
corresponding `resource` block in the configuration _but_ its `count`
44+
or `for_each` argument (or lack thereof) lacks an instance key that is
45+
tracked in the prior state.
46+
- **Update**, if there is a corresponding resource instance both declared in the
47+
configuration (in a `resource` block) and recorded in the prior state
48+
(unless it's marked as "tainted") but there are differences between the prior
49+
state and the configuration which the corresponding provider doesn't
50+
explicitly classify as just being normalization.
51+
- **Replace**, if there is a corresponding resource instance both declared in
52+
the configuration (in a `resource` block) and recorded in the prior state
53+
_marked as "tainted"_. The special "tainted" status means that the process
54+
of creating the object failed partway through and so the existing object does
55+
not necessarily match the configuration, so Terraform plans to replace it
56+
in order to ensure that the resulting object is complete.
57+
- **Read**, if there is a `data` block in the configuration.
58+
- If possible, Terraform will eagerly perform this action during the planning
59+
phase, rather than waiting until the apply phase.
60+
- If the configuration contains at least one unknown value, or if the
61+
data resource directly depends on a managed resource that has any change
62+
proposed elsewhere in the plan, Terraform will instead delay this action
63+
to the apply phase so that it can react to the completion of modification
64+
actions on other objects.
65+
- **No-op**, to explicitly represent that Terraform considered a particular
66+
resource instance but concluded that no action was required.
67+
68+
The **Replace** action described above is really a sort of "meta-action", which
69+
Terraform expands into separate **Create** and **Delete** operations. There are
70+
two possible orderings, and the first one is the default planning behavior
71+
unless overridden by a special planning behavior as described later. The
72+
two possible lowerings of **Replace** are:
73+
1. **Delete** then **Create**: first delete the existing object bound to an
74+
instance, and then create a new object at the same address based on the
75+
current configuration.
76+
2. **Create** then **Delete**: mark the existing object bound to an instance as
77+
"deposed" (still exists but not current), create a new current object at the
78+
same address based on the current configuration, and then delete the deposed
79+
object.
80+
81+
## Special Planning Behaviors
82+
83+
For the sake of this document, a "special" planning behavior is one where
84+
Terraform Core will select a different action than the defaults above,
85+
based on explicit instructions given either by a module author, an operator,
86+
or a provider.
87+
88+
There are broadly three different design patterns for special planning
89+
behaviors, and so each "special" use-case will typically be met by one or more
90+
of the following depending on which stakeholder is activating the behavior:
91+
92+
- [Configuration-driven Behaviors](#configuration-driven-behaviors) are
93+
activated by additional annotations given in the source code of a module.
94+
95+
This design pattern is good for situations where the behavior relates to
96+
a particular module and so should be activated for anyone using that
97+
module. These behaviors are therefore specified by the module author, such
98+
that any caller of the module will automatically benefit with no additional
99+
work.
100+
- [Provider-driven Behaviors](#provider-driven-behaviors) are activated by
101+
optional fields in a provider's response when asked to help plan one of the
102+
default actions given above.
103+
104+
This design pattern is good for situations where the behavior relates to
105+
the behavior of the remote system that a provider is wrapping, and so from
106+
the perspective of a user of the provider the behavior should appear
107+
"automatic".
108+
109+
Because these special behaviors are activated by values in the provider's
110+
response to the planning request from Terraform Core, behaviors of this
111+
sort will typically represent "tweaks" to or variants of the default
112+
planning behaviors, rather than entirely different behaviors.
113+
- [Single-run Behaviors](#single-run-behaviors) are activated by explicitly
114+
setting additional "plan options" when calling Terraform Core's plan
115+
operation.
116+
117+
This design pattern is good for situations where the direct operator of
118+
Terraform needs to do something exceptional or one-off, such as when the
119+
configuration is correct but the real system has become degraded or damaged
120+
in a way that Terraform cannot automatically understand.
121+
122+
However, this design pattern has the disadvantage that each new single-run
123+
behavior type requires custom work in every wrapping UI or automaton around
124+
Terraform Core, in order provide the user of that wrapper some way
125+
to directly activate the special option, or to offer an "escape hatch" to
126+
use Terraform CLI directly and bypass the wrapping automation for a
127+
particular change.
128+
129+
We've also encountered use-cases that seem to call for a hybrid between these
130+
different patterns. For example, a configuration construct might cause Terraform
131+
Core to _invite_ a provider to activate a special behavior, but let the
132+
provider make the final call about whether to do it. Or conversely, a provider
133+
might advertise the possibility of a special behavior but require the user to
134+
specify something in the configuration to activate it. The above are just
135+
broad categories to help us think through potential designs; some problems
136+
will require more creative combinations of these patterns than others.
137+
138+
### Configuration-driven Behaviors
139+
140+
Within the space of configuration-driven behaviors, we've encountered two
141+
main sub-categories:
142+
- Resource-specific behaviors, whose effect is scoped to a particular resource.
143+
The configuration for these often lives inside the `resource` or `data`
144+
block that declares the resource.
145+
- Global behaviors, whose effect can span across more than one resource and
146+
sometimes between resources in different modules. The configuration for
147+
these often lives in a separate location in a module, such as a separate
148+
top-level block which refers to other resources using the typical address
149+
syntax.
150+
151+
The following is a non-exhastive list of existing examples of
152+
configuration-driven behaviors, selected to illustrate some different variations
153+
that might be useful inspiration for new designs:
154+
155+
- The `ignore_changes` argument inside `resource` block `lifecycle` blocks
156+
tells Terraform that if there is an existing object bound to a particular
157+
resource instance address then Terraform should ignore the configured value
158+
for a particular argument and use the corresponding value from the prior
159+
state instead.
160+
161+
This can therefore potentially cause what would've been an **Update** to be
162+
a **No-op** instead.
163+
- The `replace_triggered_by` argument inside `resource` block `lifecycle`
164+
blocks can use a proposed change elsewhere in a module to force Terraform
165+
to propose one of the two **Replace** variants for a particular resource.
166+
- The `create_before_destroy` argument inside `resource` block `lifecycle`
167+
blocks only takes effect if a particular resource instance has a proposed
168+
**Replace** action. If not set or set to `false`, Terraform will decompose
169+
it to **Destroy** then **Create**, but if set to `true` Terraform will use
170+
the inverted ordering.
171+
172+
Because Terraform Core will never select a **Replace** action automatically
173+
by itself, this is an example of a hybrid design where the config-driven
174+
`create_before_destroy` combines with any other behavior (config-driven or
175+
otherwise) that might cause **Replace** to customize exactly what that
176+
**Replace** will mean.
177+
- Top-level `moved` blocks in a module activate a special behavior during the
178+
planning phase, where Terraform will first try to change the bindings of
179+
existing objects in the prior state to attach to new addresses before running
180+
the normal planning process. This therefore allows a module author to
181+
document certain kinds of refactoring so that Terraform can update the
182+
state automatically once users upgrade to a new version of the module.
183+
184+
This special behavior is interesting because it doesn't _directly_ change
185+
what actions Terraform will propose, but instead it adds an extra
186+
preparation step before the typical planning process which changes the
187+
addresses that the planning process will consider. It can therefore
188+
_indirectly_ cause different proposed actions for affected resource
189+
instances, such as transforming what by default might've been a **Delete**
190+
of one instance and a **Create** of another into just a **No-op** or
191+
**Update** of the second instance.
192+
193+
This one is an example of a "global behavior", because at minimum it
194+
affects two resource instance addresses and, if working with whole resource
195+
or whole module addresses, can potentially affect a large number of resource
196+
instances all at once.
197+
198+
### Provider-driven Behaviors
199+
200+
Providers get an opportunity to activate some special behaviors for a particular
201+
resource instance when they respond to the `PlanResourceChange` function of
202+
the provider plugin protocol.
203+
204+
When Terraform Core executes this RPC, it has already selected between
205+
**Create**, **Delete**, or **Update** actions for the particular resource
206+
instance, and so the special behaviors a provider may activate will typically
207+
serve as modifiers or tweaks to that base action, and will not allow
208+
the provider to select another base action altogether. The provider wire
209+
protocol does not talk about the action types explicitly, and instead only
210+
implies them via other content of the request and response, with Terraform Core
211+
making the final decision about how to react to that information.
212+
213+
The following is a non-exhastive list of existing examples of
214+
provider-driven behaviors, selected to illustrate some different variations
215+
that might be useful inspiration for new designs:
216+
217+
- When the base action is **Update**, a provider may optionally return one or
218+
more paths to attributes which have changes that the provider cannot
219+
implement as an in-place update due to limitations of the remote system.
220+
221+
In that case, Terraform Core will replace the **Update** action with one of
222+
the two **Replace** variants, which means that from the provider's
223+
perspective the apply phase will really be two separate calls for the
224+
decomposed **Create** and **Delete** actions (in either order), rather
225+
than **Update** directly.
226+
- When the base action is **Update**, a provider may optionally return a
227+
proposed new object where one or more of the arguments has its value set
228+
to what was in the prior state rather than what was set in the configuration.
229+
This represents any situation where a remote system supports multiple
230+
different serializations of the same value that are all equivalent, and
231+
so changing from one to another doesn't represent a real change in the
232+
remote system.
233+
234+
If all of those taken together causes the new object to match the prior
235+
state, Terraform Core will treat the update as a **No-op** instead.
236+
237+
Of the three genres of special behaviors, provider-driven behaviors is the one
238+
we've made the least use of historically but one that seems to have a lot of
239+
opportunities for future exploration. Provider-driven behaviors can often be
240+
ideal because their effects appear as if they are built in to Terraform so
241+
that "it just works", with Terraform automatically deciding and explaining what
242+
needs to happen and why, without any special effort on the user's part.
243+
244+
### Single-run Behaviors
245+
246+
Terraform Core's "plan" operation takes a set of arguments that we collectively
247+
call "plan options", that can modify Terraform's planning behavior on a per-run
248+
basis without any configuration changes or special provider behaviors.
249+
250+
As noted above, this particular genre of designs is the most burdensome to
251+
implement because any wrapping software that can ask Terraform Core to create
252+
a plan must ideally offer some way to set all of the available planning options,
253+
or else some part of Terraform's functionality won't be available to anyone
254+
using that wrapper.
255+
256+
However, we've seen various situations where single-run behaviors really are the
257+
most appropriate way to handle a particular use-case, because the need for the
258+
behavior originates in some process happening outside of the scope of any
259+
particular Terraform module or provider.
260+
261+
The following is a non-exhastive list of existing examples of
262+
single-run behaviors, selected to illustrate some different variations
263+
that might be useful inspiration for new designs:
264+
265+
- The "replace" planning option specifies zero or more resource instance
266+
addresses.
267+
268+
For any resource instance specified, Terraform Core will transform any
269+
**Update** or **No-op** action for that instance into one of the
270+
**Replace** actions, thereby allowing an operator to respond to something
271+
having become degraded in a way that Terraform and providers cannot
272+
automatically detect and force Terraform to replace that object with
273+
a new one that will hopefully function correctly.
274+
- The "refresh only" planning mode ("planning mode" is a single planning option
275+
that selects between a few mutually-exclusive behaviors) forces Terraform
276+
to treat every resource instance as **No-op**, regardless of what is bound
277+
to that address in state or present in the configuration.
278+
279+
## Legacy Operations
280+
281+
Some of the legacy operations Terraform CLI offers that _aren't_ integrated
282+
with the plan and apply flow could be thought of as various degenerate kinds
283+
of single-run behaviors. Most don't offer any opportunity to preview an effect
284+
before applying it, but do meet a similar set of use-cases where an operator
285+
needs to take some action to respond to changes to the context Terraform is
286+
in rather than to the Terraform configuration itself.
287+
288+
Most of these legacy operations could therefore most readily be translated to
289+
single-run behaviors, but before doing so it's worth researching whether people
290+
are using them as a workaround for missing configuration-driven and/or
291+
provider-driven behaviors. A particular legacy operation might be better
292+
replaced with a different sort of special behavior, or potentially by multiple
293+
different special behaviors of different genres if it's currently serving as
294+
a workaround for many different unmet needs.

0 commit comments

Comments
 (0)