|
| 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