Back
Keentune
Terraform & IaC curriculum 23 chapters
·
136 concepts
·
free
Everything the adaptive question bank can teach and test in Terraform & IaC, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
Start practising Terraform & IaC
New here? Read the Terraform & IaC guide
A free 16-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Infrastructure as code and the Terraform workflow
•
you describe the end state; Terraform derives the steps, so there is no "do this, then that"
•
applying an unchanged configuration a second time plans no changes; a config that always changes something is a bug
•
the core loop is write, review a plan, apply; the plan is the artifact a human actually reviews
•
every run reconciles three things: the configuration, the real infrastructure, and the recorded state
•
replacing an object beats mutating it, which is why Terraform's answer to an unchangeable attribute is replacement
•
Terraform core knows no cloud; every API call goes through a separately versioned provider plugin
B. Configuration syntax and file layout
•
a block is a type, zero or more quoted labels, and a body of name = expression arguments
•
all .tf files in the directory are merged into one configuration; the order you write blocks in never affects execution
•
<<EOT starts a multi-line string; <<-EOT additionally strips the shared leading indentation
•
${...} substitutes an expression, and $${ emits a literal dollar-brace for a downstream templating system
•
%{if} and %{for} inside a string produce conditional and repeated text, and ~ trims the adjacent whitespace
•
.tf.json is an equivalent generated-code syntax, and a _override.tf file merges on top of the base definition
•
string, number and bool, with automatic conversion between them wherever it is unambiguous
•
list, set and map each hold one element type; a set is unordered and de-duplicated, a map is keyed by string
•
an object gives each named attribute its own type, a tuple gives each position its own type
•
null means "argument not set", which is not the same as an empty string, zero, or an empty list
•
a value not yet computed prints as "(known after apply)" and makes everything derived from it unknown too
•
optional(string, "x") in a type constraint lets a caller omit an attribute and supplies the default
•
var. , local. , data. , module. , each. , count. , and a bare type.name for a managed resource
•
both result expressions of a ? b : c must convert to one type, so mixing a string and a list fails
•
[for ...] produces a tuple, {for k => v ...} produces an object; the brackets choose the result type
•
the if clause of a for expression drops elements, which is how filtering is expressed without a function
•
aws_instance.web[*].id collects one attribute across every instance of a resource
•
dynamic generates repeated nested BLOCKS from a collection; it cannot generate arguments or meta-argument blocks
•
path.module , path.root , path.cwd and terraform.workspace describe where and how the run is happening
•
the language has no user-defined functions; only a provider can contribute one
•
try returns the first argument that evaluates without error, can reports success as a bool
•
merge , flatten , zipmap and setproduct are how nested iteration is expressed in a language with no loops
•
file reads bytes as-is; templatefile renders a template whose only variables are the ones you pass it
•
timestamp , uuid and bcrypt return something new every run, producing a diff that never settles
•
lookup takes a fallback for a missing key, and element wraps around using the index modulo the list length
•
a variable with no default is required, and Terraform prompts for it rather than assuming a value
•
lowest to highest: default , TF_VAR_ environment, terraform.tfvars , terraform.tfvars.json , *.auto.tfvars in lexical order, then -var and -var-file
•
a condition plus an error_message rejects bad input during plan instead of failing halfway through an apply
•
sensitive = true redacts the value in CLI output; the value is still written to state in cleartext
•
ephemeral = true keeps the value out of state and plan files entirely, at the cost of where it may be used
•
with nullable = false , an explicitly passed null falls back to the default instead of arriving as null
•
a local is a named expression private to its module; no CLI flag or tfvars file can override one
•
a child module's resources are unreachable from the parent unless an output block exposes the value
•
an output computed from a sensitive value errors unless the output is itself marked sensitive
•
depends_on in an output block defers it when the real dependency is not visible in the expression
•
root-module outputs are printed by apply, readable via terraform output , and stored in the state file
H. Resources and resource behavior
•
per resource Terraform plans exactly one of: create, update in place, destroy and re-create, or no-op
•
an attribute the remote API cannot change after creation makes an edit plan a replacement, not an update
•
state binds a configuration address to a remote object, so renaming the block reads as destroy plus create
•
deleting a resource block is an instruction to destroy the object, not to stop managing it
•
terraform_data stores arbitrary values and triggers_replace , superseding null_resource since 1.4
•
the provider argument picks a named alias configuration for one resource instead of the default
•
count makes a numerically indexed collection whose instances are name[0] , name[1] , and so on
•
removing an item from the middle of a counted list re-indexes it and plans a replacement for every item after the gap
•
for_each addresses instances by map key, so deleting one entry touches only that instance
•
a list is rejected; toset() converts one, and each element then becomes its own key
•
the keys must be known during plan, so they cannot be derived from an attribute of a resource this run creates
•
count = var.enabled ? 1 : 0 toggles a resource, and every reference to it then needs the [0] index
J. lifecycle and depends_on
•
builds the replacement first, and Terraform propagates the setting to the resource's dependencies so the graph stays acyclic
•
rejects any plan that would destroy the object, but deleting the resource block deletes the guard along with it
•
stops a drifting attribute from planning an update; all ignores every attribute after creation
•
forces replacement when a referenced resource or attribute changes, even though nothing about this resource did
•
assumptions checked before the change and guarantees checked after, both of which fail the run
•
lifecycle arguments are read while building the graph, too early for variables or any computed expression
•
references create ordering automatically; depends_on is only for a dependency Terraform cannot see in an expression
•
a data block queries something that already exists and never creates, changes or destroys it
•
data sources normally resolve while planning, so their values appear as concrete numbers in the diff
•
an unknown argument or a depends_on pushes the read to apply time and makes everything downstream unknown
•
looking up an object the same run is creating is a race; reference the resource attribute instead
•
terraform_remote_state reads another configuration's outputs, and requires read access to that entire state file
L. Providers and version constraints
•
a provider is identified by its registry source address; the local name is just a label you chose
•
>= , <= , != , and the pessimistic ~> which allows the rightmost component to increase
•
.terraform.lock.hcl records the selected versions and their checksums, and belongs in version control
•
terraform init honors the lock file; only -upgrade re-selects newer versions within the declared constraints
•
alias configures the same provider twice, for example two regions, chosen per resource or passed to a module
•
required_version in the terraform block constrains the CLI itself, which is a separate concern from provider versions
•
writing aws_subnet.a.id inside another resource is what orders the two; nothing else is needed
•
the graph must be acyclic, so two resources referencing each other is a hard error, not a slow resolution
•
independent nodes are visited concurrently, ten at a time by default, adjustable with -parallelism
•
destruction walks the graph backwards, so dependents are removed before the things they depend on
•
an ordering failure that only happens sometimes almost always means a real dependency has no reference expressing it
•
the mapping from each configuration address to a real object's ID, plus the attribute values last seen
•
without it Terraform cannot distinguish "never created" from "created and since changed"
•
every attribute including passwords is stored unencrypted, so the state file is itself a secret
•
the stored attributes are a refreshed cache for diffing and performance; the remote API is the source of truth
•
the format is internal and versioned; use the terraform state commands rather than a text editor
•
state in git means merge conflicts, leaked credentials, and no locking between collaborators
•
the default local backend writes terraform.tfstate beside the configuration; any team needs a shared remote backend
•
the backend is configured before expressions are evaluated, so it cannot reference variables or locals
•
leave the credentials out of the block and supply them with -backend-config at init time
•
init -migrate-state copies existing state into a new backend, -reconfigure abandons the old association
•
a lock prevents two concurrent runs from writing conflicting state, and not every backend implements one
•
clears a lock stranded by a crashed run, and corrupts state if used while another run is genuinely alive
•
a workspace is an additional named state for the same configuration and the same backend
•
terraform.workspace yields the current name, which is how one configuration varies per environment
•
every configuration starts in a workspace named default , which cannot be deleted
•
same backend and same credentials for every workspace, which is why prod is usually a separate configuration instead
•
a plan first reconciles state against reality, then compares that result to the configuration
•
+ create, - destroy, ~ update in place, -/+ destroy then create, +/- create then destroy, <= read
•
the comment printed beside one attribute is what identifies which change caused the replacement
•
plan -out then applying that file guarantees you apply exactly the change you reviewed, not a re-planned one
•
a saved plan holds variable and attribute values in cleartext, so it is an artifact that needs protecting
•
-refresh-only records drift without changing infrastructure; -refresh=false skips reading the provider at all
•
-target deliberately skips part of the graph and is documented as an exceptional measure, not a workflow
•
"objects have changed outside of Terraform" reports out-of-band edits, which the next apply will revert unless ignored
R. Import and refactoring
•
a declarative import block adopts an existing object on the next apply and shows up in the plan for review
•
plan -generate-config-out writes starter HCL matching an imported object so you do not hand-transcribe it
•
an import only records state; if the configuration does not match, the very next plan proposes changes
•
renaming a resource or wrapping it in a module without a moved block plans a destroy and a create
•
drops a resource from state while leaving the real object alive, the reviewable form of state rm
•
the imperative equivalents, which rewrite state immediately with no plan, no review and no audit trail
•
the working directory is the root module; every module block calls a child module with its own variables and outputs
•
local path, registry, Git, HTTP archive and object storage, each with its own address syntax
•
the version argument works for registry sources; a Git source pins with a ref in the URL instead
•
changing a module's source or version requires terraform init before the next plan will see it
•
a reusable module should inherit or be passed providers; declaring its own makes it impossible to remove cleanly
•
a flat root wiring small modules together beats a deep tree threading variables down through layers
•
a module that only forwards arguments adds indirection without encoding a decision; not every repetition needs an abstraction
•
the docs' own position: provisioners are unmodelable side effects, to be used only when the provider offers nothing
•
a failed creation-time provisioner marks the object tainted, so the next apply replaces it
•
when = destroy runs before deletion and may only reference self , count.index and each.key
•
a provisioner cannot name its own parent resource, because that would be a self-reference; it uses self instead
•
Terraform has no model of what the script did, so an unchanged config will never re-run it to repair anything
U. Secrets and sensitive data
•
the marker suppresses console output; it does not encrypt state, the plan file, or provider logs
•
sensitivity spreads through every expression that touches the value, which is why an unrelated-looking output starts redacting
•
nonsensitive() strips the mark and is an explicit assertion that the derived value is safe to print
•
ephemeral resources, ephemeral variables and write-only arguments exist only during the run and are never persisted
•
reading a secret through a data source puts the plaintext in state; the fix is ephemeral or write-only, not a different data source
•
credentials belong in the environment or a secret manager, and secret-bearing *.tfvars belong in .gitignore
V. Validation, testing and policy
•
fmt rewrites files to canonical style; validate checks syntax, types and references without calling any provider API
•
validation runs against the installed providers' schemas, so it cannot run before terraform init
•
.tftest.hcl files hold run blocks, each a plan or an apply carrying assert conditions
•
mock_provider supplies fake attribute values so a test can assert on a plan without creating real infrastructure
•
a failed check assertion is a warning that does not stop the run, unlike a postcondition which fails it
•
Sentinel and OPA evaluate the JSON from terraform show -json , so the gate sits between plan and apply
W. CI/CD and collaboration
•
the pull request is where the plan gets reviewed, and the merge is what authorizes the apply
•
-input=false and -auto-approve belong in a pipeline and nowhere near an interactive terminal
•
plan -detailed-exitcode returns 0 for no changes, 1 for error and 2 for changes, which is how a pipeline branches
•
CI should assume a role through OIDC for the length of the run rather than store long-lived keys
•
one enormous state is slow to plan and dangerous to apply; split by rate of change and join the pieces with outputs
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practising Terraform & IaC
All about Terraform & IaC practice
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.
© 2026 SportaApp LLC