Everything the adaptive question bank can teach and test in Terraform & IaC, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
A. Infrastructure as code and the Terraform workflow
Because the steps are derived rather than written, the same configuration produces different actions depending on what already exists — creating everything on an empty account and nothing on a converged one. The skill being exercised is describing an end state precisely, not sequencing operations.
A second run over unchanged input should find nothing to do, so a configuration that proposes a change every time is broken rather than noisy. The usual culprits are an impure function or a remote attribute the provider normalizes differently from how you wrote it.
The middle step is the one that carries the value: the plan is a reviewable artifact describing precisely what will happen, before anything happens. Skipping straight to applying discards the only safety mechanism in the loop, which is why pipelines separate the two deliberately.
Configuration, reality and the recorded snapshot are three separate things, and any two of them can disagree. Almost every confusing result — a resource proposed for creation that exists, a change that reverts itself — is one pair diverging, so the diagnostic question is always which pair.
Replacing rather than mutating is the underlying philosophy, which is why an attribute the remote API cannot change in place produces a destroy-and-create rather than a failure. That behavior looks alarming until you recognize it as the model doing exactly what it promises.
The core knows nothing about any cloud; every remote call goes through a plugin with its own version and its own release cycle. That separation explains why upgrading the tool and upgrading a provider are unrelated operations, and why a missing feature is usually a provider question.
B. Configuration syntax and file layout
A type, optional labels, and a body of named arguments is the entire grammar, which is why everything from a resource to a variable to the backend looks structurally identical. The labels are what give a block its address, so they are identity rather than decoration.
Every file in the directory is merged into one configuration and execution order comes from the dependency graph, so splitting into files is purely organizational. Reading a configuration top to bottom as a sequence is the habit to unlearn — nothing runs in the order it is written.
The plain form preserves every character including leading whitespace; the indented variant strips the common indentation so a block can be nested inside the configuration without embedding that nesting in the string. Choosing the first inside an indented block is how stray spaces reach a script.
Substitution happens before the string is handed anywhere, so a template meant for a downstream system must escape its own delimiters or they are consumed here. That is the source of the empty-looking variable in a shell script or a cloud-init file that was written correctly.
Conditional and repeated text inside a string is what lets a rendered file vary without assembling it from concatenation. The whitespace-trimming marker matters more than it looks: without it, every directive leaves a blank line in the output, which breaks formats that care.
The JSON form is an equivalent syntax intended for generated configuration rather than for people, and the override mechanism merges on top of an existing definition. Both exist for machine and layering use cases, and both make a configuration harder to read when used by hand.
C. Types and values
Conversion between the three happens automatically wherever it is unambiguous, which is why a number can be supplied where a string is expected. That convenience hides the type until something depends on it — a comparison, a map key, a strict variable constraint — and then the distinction reappears.
All three hold one element type and differ in addressing: ordered by position, unordered and deduplicated, or keyed by string. The middle one surprises people most, because converting a list to it silently discards duplicates and any ordering the source had.
Unlike collections, each attribute or position carries its own type, which is what allows a variable to describe a record rather than a uniform bag. That is what makes a typed object constraint a usable interface contract for a module's inputs.
It means the argument was not set, which is distinct from an empty string, a zero or an empty collection — and providers frequently treat those four differently. Passing one where you meant another is a common way to get a default you did not want.
A value not yet computed propagates: anything derived from it is also unknown, and enough of them make a plan unreviewable. That propagation is why a single attribute that cannot be known until apply can turn a precise diff into a page of placeholders.
Marking an attribute optional inside a type constraint lets callers omit it and supplies a default in its place, which keeps a module's interface from requiring every field. It is what makes a large typed object practical as a variable rather than punishing.
D. Expressions
Each prefix names a different kind of thing, and a managed resource is the one with no prefix at all — just its type and name. Knowing the set is what makes an error about an unrecognized reference legible, because it almost always means the wrong prefix for the kind of object.
Both branches must converge to a single type, so a conditional returning a string on one side and a collection on the other fails rather than picking per evaluation. That constraint is what keeps the result's type knowable during planning, before either branch is chosen.
The surrounding brackets decide the result type, not the body, so producing a keyed result requires the object form and the key-value arrow. Reaching for the wrong brackets produces a positional collection where a keyed one was needed, which fails later at the point of use.
The filtering clause drops elements as the comprehension runs, which is how selection is expressed in a language with no loop and no filter function taking a predicate. Combining it with the object form is the standard way to build a map from a subset.
Collecting one attribute across every instance produces a list in index order, which is the compact way to hand all of something to another resource. It works over a counted resource naturally and over a keyed one with a conversion, which is the distinction that trips people up.
It generates repeated nested blocks from a collection, and only those — arguments and meta-arguments cannot be produced this way, because they are read before expressions are evaluated. That limitation is the reason a dynamic lifecycle or a dynamic count is impossible rather than merely unsupported.
These describe the run rather than the infrastructure: where the module lives, where the root is, where the process was started, and which named state is active. Using the wrong path reference is the usual cause of a file that resolves in one invocation and not another.
E. Built-in functions
There is no way to define one, so every abstraction is expressed with locals, modules and the built-in set. That constraint keeps configurations comparable across teams, and it is why nested iteration ends up written as a combination of the reshaping functions rather than as a helper.
One returns the first expression that evaluates successfully and the other reports whether an expression would succeed at all. They exist because there is no exception handling, and they are the standard way to tolerate an optional attribute without writing a lookup chain.
Merging, flattening, pairing keys with values and producing cross-products are how nested iteration is expressed in a language with no loops. Most awkward configuration is a combination of these that could have been one clearer expression, so knowing the set is worth more than it looks.
One reads bytes verbatim and the other renders a template whose variables are exactly those you pass — no ambient access to the configuration's own values. That isolation is deliberate: a template cannot reach out, so its inputs are visible at the call site.
A function returning something new on each evaluation produces a difference every run, so the configuration never converges and every plan shows a change. It is the classic self-inflicted drift, and the remedy is to store the generated value as a resource rather than recompute it.
One accepts a fallback for a missing key and the other wraps around by taking the index modulo the length. The second is a silent source of surprise: an index past the end returns an element rather than failing, so an off-by-one produces wrong output rather than an error.
F. Input variables
Omitting a default makes the variable required, and an interactive run prompts rather than assuming. In automation that prompt is a hang or a failure, which is why pipelines disable input and why a newly added required variable breaks a pipeline that worked yesterday.
Several sources can set the same variable and the order between them is fixed, with command-line flags last and therefore winning. Knowing the order is what settles the question of why a value in one file is being ignored — something later in the sequence set it too.
A condition paired with a message rejects bad input while planning rather than failing partway through an apply, which is the difference between an error and a half-built environment. It is also documentation: the message states the constraint in the place a caller will read it.
The marker suppresses the value in console output and nothing more — the value still reaches the state file in cleartext. Treating it as a secrecy mechanism is the mistake it most often causes, because the redaction looks like protection.
Declaring a value ephemeral keeps it out of state and plan files entirely, which is the genuine protection the display marker does not provide. The trade is that such a value may only be used in places that do not persist it, so it cannot simply be marked on an existing variable.
With this set, an explicitly passed empty value falls back to the default instead of arriving as one, which removes an entire class of conditional handling inside the module. It changes what a caller passing nothing and a caller passing null mean — deliberately making them the same.
G. Locals and outputs
A local is a named expression private to its module and cannot be overridden from outside by any flag or file. That is the distinction from a variable: one is an input surface, the other is internal factoring, and using a variable for the second exposes something callers can now change.
Nothing inside a child module is reachable from the parent unless it is deliberately exposed, which makes the set of outputs the module's public interface. That encapsulation is why adding an output is a versioned interface change rather than a cosmetic edit.
Sensitivity travels through expressions, so an output derived from a protected value must itself be marked or the run fails. The error is deliberate: it forces an explicit decision rather than quietly printing something that came from a secret.
Ordering usually comes from references, and this exists for the case where the real dependency is not visible in the expression — typically a side effect. Needing it in an output is a signal worth reading, because it usually means something upstream is not modeled.
Top-level outputs are printed on apply, readable through the command line afterwards, and stored in the state file. That last part matters for secrets: an output is another place a sensitive value comes to rest, and it is the one people forget when auditing.
H. Resources and resource behavior
Exactly one of four outcomes is chosen per object, and reading which one is the first thing to check in a diff. The destructive pair is what deserves attention, because an in-place update and a replacement can look similar in the output and differ completely in consequence.
Some attributes cannot be changed after creation because the remote API does not permit it, so editing one means destroying and rebuilding. The tool reports this rather than failing, which is correct and is also why a one-word edit can propose deleting a database.
The binding is between a configuration address and a remote object, so renaming the block breaks that association — the old address disappears and a new one has nothing behind it. That is why a pure rename plans a destroy and a create unless the move is declared.
Deleting a block is an instruction to remove the object, not an instruction to stop managing it. Those two intentions are genuinely different and there is a separate construct for the second, which is what to reach for when handing a resource to another team or another configuration.
It stores arbitrary values and can be forced to replace when something else changes, which covers the cases that once required a null resource. Using it deliberately is fine; using it to hold logic the provider should be expressing is how a configuration becomes unreadable.
Naming an alias on a single resource is how one configuration spans two regions or two accounts without splitting into separate runs. The alias must be defined and, for a module, passed in explicitly — inheritance only covers the default configuration.
I. count and for_each
Instances are addressed by position, which is simple and ties identity to ordering. That coupling is the whole difficulty: identity is derived from where something sits in a list rather than from what it is, so anything that changes the ordering changes every identity after it.
Removing an item from the middle re-indexes everything after it, so each of those instances is now bound to a different address and is planned for replacement. The plan looks catastrophic and is technically correct — the addresses really did all change.
Addressing by key means an instance's identity comes from its own name rather than from its neighbors, so removing one entry touches exactly one instance. That stability is the reason it is preferred for anything whose membership will change.
A list is rejected because positional ordering is exactly what this construct is avoiding; converting one makes each element its own key. That conversion is also a deduplication, which is worth knowing when the source list legitimately contains repeats.
The keys decide how many instances exist, so they must be known while planning — which rules out deriving them from an attribute of something this run will create. The error names the unknown value, and the fix is usually a separate run or a value known in advance.
Setting the count from a condition is the idiomatic toggle, and it changes the resource into a collection — so every reference to it needs an index even when there is at most one. That ripple through the rest of the configuration is the cost of the pattern.
J. lifecycle and depends_on
Building the replacement before removing the original avoids a gap in availability, and the setting propagates to dependencies so the ordering graph stays resolvable. It also requires that both can exist at once, which unique names and fixed identifiers frequently prevent.
It rejects any plan that would remove the object, which protects against an accidental attribute change proposing a replacement. What it does not protect against is deleting the block itself, because that removes the guard in the same edit — the protection lives with the thing protected.
It stops a drifting attribute from proposing an update, which is the right answer when something outside the configuration legitimately owns that field. Ignoring everything after creation is the blunt version, and it means the configuration no longer describes the object in any enforceable way.
Replacement is forced by a change elsewhere, even though nothing about this object differs — which is how a compute instance is rebuilt when its image or its startup script moves. It expresses a dependency that the remote API does not represent as an attribute.
One states an assumption checked before the change and the other a guarantee checked after, and both fail the run rather than warning. They put the assertion next to the thing it is about, which is why they read better than a separate validation somewhere else.
These arguments are read while the graph is being built, before any expression is evaluated, so they cannot reference a variable or anything computed. That timing is the reason the restriction exists, and it is why a conditional lifecycle setting is impossible rather than merely awkward.
References create ordering automatically, so needing an explicit declaration means the dependency is invisible in the expressions — usually a side effect such as a permission that must exist first. Reaching for it routinely hides the real structure and slows the graph down.
K. Data sources
These query something that already exists and never create, modify or remove it, which makes them safe to add and to remove. The failure mode is not damage but absence — a query matching nothing fails the run, and a query matching several may pick one arbitrarily.
Resolving while planning is what puts concrete values in the diff rather than placeholders, so the review shows real numbers. That is also why a query against something slow or rate-limited makes every plan slower, including ones that change nothing.
An unknown argument or an explicit dependency pushes the read to apply time, and everything computed from it becomes unknown in the plan. That is the usual explanation for a diff full of placeholders where it used to show values — something upstream made the query unresolvable early.
Querying an object the same run is creating is a race: at plan time it does not exist, and the result is either a failure or a stale answer. Referencing the resource's own attribute is both correct and what establishes the ordering, so it fixes two problems at once.
Reading another configuration's outputs requires read access to that entire state file, which contains every attribute of everything it manages, including secrets. That access requirement is the reason to prefer a published value in a parameter store for anything crossing a trust boundary.
L. Providers and version constraints
A provider is identified by its registry address, and the local name is a label you chose that happens to default to something familiar. Two providers can share a short name from different namespaces, which is exactly why the address rather than the label is the identity.
The comparison operators do what they look like; the pessimistic one allows only the rightmost component to increase, which is how a minor or patch range is expressed compactly. Writing an exact pin instead is occasionally right and generally makes upgrades a manual sweep.
Recording the selected versions and their checksums is what makes two machines resolve identically, so it belongs in version control alongside the configuration. Without it, the constraint alone permits a range and two runs can legitimately install different builds.
Initialization honors what the lock file records, so newer versions inside the declared range are not picked up until an upgrade is requested. That is deliberate — it means running the setup step is safe and does not silently move your dependencies.
Configuring the same provider more than once is how one run spans two regions or two accounts, chosen per resource or passed into a module. For a module the configuration must be handed in explicitly, because only the default is inherited.
This constrains the tool itself rather than any provider, which is a separate concern with a separate failure mode: a machine with the wrong version refuses to run at all. Pinning it is what stops a state file being written by a newer version that older ones cannot read.
M. The dependency graph
Mentioning one object's attribute inside another is what orders them, so the ordering is a consequence of the data flow rather than something declared separately. That is why a correctly written configuration usually needs no explicit ordering at all, and why adding it is a smell.
Acyclicity is a hard requirement, so two objects referencing each other is an error rather than something resolved by iteration. The message names the cycle, and the fix is almost always to break the mutual reference by introducing a third object or moving an attribute.
Independent nodes are visited concurrently up to a default width, which is why a large configuration applies faster than the sum of its parts. Lowering that width is the remedy for hitting a remote API's rate limit, which presents as sporadic failures rather than as throttling.
Teardown walks the graph backwards so that dependents are removed before whatever they depend on, which is the only order the remote APIs will accept. It is also why a destroy can fail on an object that something outside the configuration is still using.
An ordering failure that only happens sometimes is the signature of a real dependency with no reference expressing it — the parallel walk happens to get the order right often enough to hide it. Adding a retry treats the symptom; expressing the dependency removes it.
N. State
It records which real object each configuration address refers to, along with the attribute values last observed. Those two jobs are different: the mapping is irreplaceable and the values are a cache, which is why losing state is serious and refreshing it is routine.
Without a record there is no way to distinguish something that was never created from something created and since changed, so every run would either recreate everything or trust the configuration blindly. The mapping is what makes a diff possible at all.
Every attribute is stored as it was read, including passwords, keys and certificates, so the file is itself a credential. That single fact decides where it may be stored, who may read it, and why a remote backend's access control is a security control rather than a convenience.
The stored values exist for diffing and speed, and the remote API remains authoritative — which is why a refresh can change them without any configuration edit. Reading state to answer what something is currently set to is answering from a snapshot.
The format is internal and versioned, so an edit that looks correct can be structurally invalid or silently inconsistent. The dedicated commands exist for every legitimate manipulation, and they maintain the invariants a text editor cannot see.
Putting it in version control produces merge conflicts on a file that cannot be merged, publishes every secret it holds, and provides no locking between collaborators. Each of those alone is disqualifying, which is why the remote backend exists.
O. Backends and locking
The default writes beside the configuration, which is fine for one person experimenting and impossible for a team — there is no sharing and no locking. Moving to a shared backend is the first infrastructure decision a collaborative configuration requires.
The backend is configured before any expression is evaluated, so it cannot reference a variable or a local. That ordering is why the block contains literals only, and it is the reason the partial-configuration mechanism exists at all.
Leaving credentials and environment-specific values out of the block and supplying them at initialization is what lets one configuration target several backends. It also keeps secrets out of the committed file, which the block's literal-only rule would otherwise force.
One copies the existing state into the new location and the other abandons the association and starts fresh. Choosing the second by accident is how a configuration ends up planning to create everything it already manages, with the old state still sitting where it was.
A lock stops two concurrent runs writing conflicting state, which is the failure that corrupts the mapping rather than merely confusing it. Not every backend implements one, so the guarantee is a property of the backend chosen rather than of the tool.
It clears a lock stranded by a crashed run, and it will happily clear one held by a run that is still alive — at which point two processes are writing the same file. The identifier it requires is the only safeguard, which is why it should be read rather than copied blindly.
P. Workspaces
A workspace is another named state for the same configuration and the same backend, which makes it cheap to create and limited in what it separates. It is a state-level distinction, not an isolation boundary, and treating it as the latter is the usual error.
Reading the current name is how one configuration varies by environment, typically by indexing a map of per-environment settings. It also makes the configuration depend on an invisible ambient value, so a run in the wrong workspace produces a valid plan against the wrong environment.
Every configuration starts in one named default that cannot be removed, so a workspace-aware configuration has to decide what that name means. Leaving it undefined is how a run that forgot to select an environment falls through to something unintended.
Every workspace shares the same backend and the same credentials, so a mistake has the whole set within reach and access cannot be granted per environment. That is why production usually gets its own configuration and its own backend rather than a workspace.
Q. Plan and apply
Two phases: reconcile the record against reality, then compare the result with the configuration. Knowing the order explains why a plan can report drift and a change in the same run, and why disabling the first phase makes the second compare against possibly stale values.
Each symbol is a distinct action and two of them destroy something, so reading them is the difference between approving an update and approving a rebuild. The pair that destroys and recreates and the pair that creates then destroys differ in availability, which is the whole point of the distinction.
When a replacement is proposed, one attribute is annotated as the cause — and that annotation is the actionable part of the diff. Without reading it, the only question available is whether to accept the replacement; with it, the question becomes whether that attribute needed to change.
Saving the plan and applying that file guarantees the change applied is the one reviewed, rather than a fresh plan computed against whatever the world looks like now. In a pipeline that gap between review and apply is exactly where an unreviewed change can appear.
A saved plan contains variable values and attribute values in cleartext, so it is a secret-bearing artifact that happens to look like a build output. Pipelines that pass it between stages need to treat it accordingly, which is easy to overlook because it is intermediate.
One records drift without proposing any change to infrastructure, and the other skips reading the provider at all. They serve opposite purposes — investigating what moved, and planning quickly against a large configuration — and neither is a general default.
Restricting the run to part of the graph deliberately skips dependencies, which is why the documentation frames it as an exceptional measure. Using it routinely produces a state that has never been reconciled as a whole, and the divergence surfaces at the worst possible moment.
Reporting that objects changed outside the configuration is a warning that the next apply will revert them, which is either exactly right or exactly wrong depending on who made the change and why. Either way it is information that should reach someone rather than being applied past.
R. Import and refactoring
Declaring the adoption in configuration means it appears in the plan and can be reviewed before anything is recorded, unlike the command-line form which acts immediately. That reviewability is the whole reason the declarative form exists.
Generating starter configuration from an imported object removes the transcription step, which is where mistakes enter — a missed attribute reads as a change to make. The output is a starting point rather than a finished file, and it usually needs tidying before it is committed.
Adoption records the mapping and nothing else, so if the configuration does not already describe the object accurately the very next plan proposes changing it to match. That is why importing without first writing the configuration produces an alarming diff against something that was fine.
Renaming or re-nesting without declaring the move breaks the address binding, so the plan proposes destroying the old address and creating the new one. Declaring it turns a destructive plan into a bookkeeping change, and it stays in the configuration for the benefit of anyone else running it.
Dropping something from the record while leaving the real object alive is the reviewable form of that operation — it appears in the plan rather than happening immediately. It is what to use when handing a resource to another configuration or another team.
The imperative equivalents rewrite the record straight away, with no plan, no review and nothing left in the configuration to show it happened. That invisibility is the argument for the declarative forms, particularly on a shared configuration where someone else will wonder.
S. Modules
The working directory is the root and every call creates a child with its own inputs and outputs, which is the only encapsulation boundary in the language. Everything about module design follows from that: what crosses the boundary is exactly the variables and outputs declared.
Local paths, a registry, version control, an archive over HTTP and object storage each have their own address syntax and their own versioning story. Choosing one is largely choosing how the module will be versioned and who can reach it.
The version argument works for registry sources, and a version-control source is pinned by a reference inside its own address instead. Mixing them up produces either an ignored argument or an error, and an unpinned version-control source silently follows a moving branch.
Changing a module's source or version requires re-running initialization before the change takes effect, because modules are fetched during that step. A plan that stubbornly shows the old behavior after a version bump is almost always this.
A module declaring its own provider configuration cannot be removed cleanly, because the configuration is needed to destroy what it created. Inheriting or accepting a passed configuration keeps the module composable, which is the difference between reusable and merely shared.
A flat root wiring small modules together keeps every input visible at one level, while a deep tree threads values down through layers that do nothing with them. The second looks tidier in a diagram and is much harder to change.
A module that only forwards its arguments adds a layer of indirection without encoding any decision, so it costs readability and buys nothing. The test is whether the module makes a choice a caller would otherwise have to make — repetition alone is not sufficient reason.
T. Provisioners
The documentation's own position is that these are unmodelable side effects, invisible to planning and to the dependency graph. That is not stylistic advice: nothing about what they did is recorded, so the tool cannot reconcile it later, which undermines the guarantee the rest of the system provides.
A failed creation-time step marks the object for replacement on the next run, because the tool has no way to know how far the script got. That is the safe behavior and it is also surprising — a script failing at the end destroys an object that was otherwise fine.
Running before deletion is the only hook for cleanup that the remote API does not perform, and it may reference almost nothing — its own object and its iteration keys. That narrow scope exists because everything else may already be gone by then.
A provisioner cannot name its own parent, because that would be a cycle in the graph, so a special reference stands in for it. The restriction is structural rather than a syntax rule, which is why no rearrangement of the expression avoids it.
Nothing about what the script did is recorded, so an unchanged configuration will never run it again — including to repair something that has since drifted. That makes it a one-shot action at creation time rather than a mechanism for maintaining anything.
U. Secrets and sensitive data
Suppressing console output does not encrypt the state file, the saved plan, or provider logs, all of which still contain the value. The marker reduces shoulder-surfing and accidental pasting, and it is routinely mistaken for a storage guarantee.
Sensitivity spreads through every expression that touches the value, which is why an output nobody considered secret suddenly redacts itself. That propagation is conservative on purpose — it errs toward hiding — and it is why removing the mark has to be an explicit act.
Stripping the mark is an assertion that the derived value is genuinely safe to show, such as the length of a secret rather than the secret. It is a deliberate override, which is the right shape: the tool cannot judge safety, so it makes you say so.
Values declared to exist only during the run are never written to the state or the plan, which is the real protection rather than a display change. The constraint is where they may be used — anything that would persist them is rejected, by design.
Fetching a secret through an ordinary query writes the plaintext into state, so the secret manager has protected it right up to the moment it arrives. Changing which store it comes from does not help; the fix is a mechanism that never persists it.
Credentials belong in the environment or a secret store, and any variables file carrying one belongs in the ignore list. The failure is permanent in a way most are not: once a secret is in the history, removing the file does not remove it.
V. Validation, testing and policy
One rewrites files to canonical style and the other checks syntax, types and references without contacting any remote API. Both are fast enough to run on every commit, and neither can tell you whether the infrastructure described is correct — only whether the configuration is coherent.
Checking types and references requires the installed providers' schemas, so it cannot run before initialization has fetched them. That is why a pipeline that validates in a clean checkout has to initialize first, even though nothing is being applied.
Test files hold run blocks, each of which is a plan or an apply carrying assertions, which makes them a real execution rather than a static analysis. That is what lets them catch a wrong expression that type-checks perfectly.
Supplying fake attribute values lets a test assert on a plan without creating anything, which makes module testing fast and free. The trade is that the mock is a model of the provider, so it verifies your configuration's logic rather than the provider's behavior.
A failed assertion here is a warning and does not stop the run, which is the opposite of a postcondition. That difference is the reason to choose one: a warning for something worth knowing, a failure for something that must not proceed.
Policy engines evaluate the machine-readable form of the plan, so the gate sits between planning and applying and can refuse changes on organizational rules the language itself cannot express. It is the mechanism behind a rule like never open this port to the world.
W. CI/CD and collaboration
The pull request is where the plan is reviewed by a human and the merge is the authorization to apply it, which maps the change-management process onto the tooling. It also means the plan has to be posted somewhere the reviewer will actually read.
Disabling input and skipping confirmation belong in a pipeline, where there is nobody to prompt, and nowhere near an interactive session. Reaching for the confirmation-skipping flag by habit at a terminal removes the last review step in the whole workflow.
Three distinct exit codes let a pipeline distinguish no changes from an error from pending changes, which is what makes a plan usable as a conditional step. Without it the only signal is success or failure, and no-changes and changes look identical.
Assuming a role for the duration of the run removes the stored long-lived key entirely, which is the single highest-value change available to a pipeline's security posture. The credential that does not exist cannot be leaked from the build logs.
One enormous state is slow to plan and dangerous to apply, because every run touches everything. Splitting by rate of change — networking apart from applications — and joining the pieces through published outputs bounds what any single mistake can reach.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.