This course’s vocabulary spans three layers: Terraform core (resource, provider, state, module, plan/apply/destroy) that works the same on every cloud; Terragrunt (unit, dependency, root config, stacks) that keeps many environments DRY; and AWS-specific pieces (the S3 backend’s native locking, IAM role assumption, Secrets Manager) that only make sense once you know which provider you’re reading. When a lesson uses a term you’re unsure about, or unsure whether it’s still current for Terraform 1.16 / Terragrunt 1.1, look it up here.
terraform apply executes the actions in a plan against real infrastructure — the only core-workflow command that actually creates, updates, or destroys anything; given no saved plan file it generates one first and asks for confirmation.
A block inside the AWS provider config that has the provider assume an IAM role (cross-account or elevated) for every API call it makes, instead of using the invoking credentials directly — the standard way to run Terraform against a different account than the one your CI role lives in.
A zero-argument data source returning the account ID, user ID, and ARN of whatever credentials the AWS provider is currently using — the standard way to confirm which account a plan is about to run against before you apply.
Registers an OIDC identity provider (e.g. GitHub Actions’ token issuer, token.actions.githubusercontent.com) with IAM so a CI pipeline can assume a role via short-lived, federated tokens instead of long-lived AWS access keys stored as CI secrets.
The plugin that decides where Terraform’s state is stored and how operations against it are locked — configured once in a terraform { backend "s3" {} } block; changing the block’s arguments requires re-running terraform init (with -migrate-state to move existing state).
terragrunt catalog browses a configured set of module repositories and interactively scaffolds a new unit from one — writing its terragrunt.hcl and starter inputs for you — instead of hand-writing a unit from scratch every time.
A static-analysis scanner (Bridgecrew/Palo Alto) that runs against Terraform source before any plan, checking built-in policies (public S3 buckets, unencrypted volumes, open security groups, …) — run via checkov -d . or its GitHub Action, which must be pinned to a specific ref rather than @master since it can introduce breaking policy changes on any push.
Declared in a module’s required_providers block, it lists the named provider aliases (e.g. aws.east, aws.west) the module expects its caller to pass in via a providers = {} argument — without it, an aliased provider can’t cross a module boundary at all.
The older, index-based way to turn a single resource/module block into N near-identical copies (resource.name[0], [1], …) — fragile to reordering, because removing a middle element from the source list shifts every index after it and forces those resources to be destroyed and recreated.
The lifecycle { create_before_destroy = true } meta-argument flips the default destroy-then-create replacement order to create-first — required whenever the old resource must keep serving traffic, or keep existing because something else still references it, until its replacement exists.
A read-only lookup (data "aws_ami" "..." {}) that fetches information about a resource Terraform doesn’t manage — either something that already exists (a shared VPC) or a provider computation (an AMI ID matching filters) — and can never be created, updated, or destroyed by Terraform itself.
A block inside the AWS provider config applying a fixed set of tags to every taggable resource the provider manages, merged with (and overridden by) any tags set directly on a resource — the standard way to guarantee a cost-center or owner tag without repeating it on every resource block.
A flag on terraform plan that changes its exit code from the plain 0/1 to 0 (no changes), 1 (error), or 2 (changes present) — what a CI pipeline actually checks to decide whether a plan needs a human approval step or can be skipped.
A Terragrunt dependency block reads another unit’s outputs as inputs (after running its own plan/apply first); the plural dependencies block instead just ensures ordering against one or more units without exposing their outputs.
terraform destroy (or apply -destroy) plans and executes the removal of every resource currently tracked in state — the same plan/apply pipeline as normal changes, just with every action set to delete.
Any difference between what’s in state and what actually exists in AWS — caused by a manual console change, another tool, or an out-of-band process — that only terraform plan/refresh surfaces, since Terraform never polls continuously on its own.
Before S3-native locking, the S3 backend’s dynamodb_table argument named a DynamoDB table (partition key LockID) used to hold a lock item during operations — still supported, but a new backend no longer needs one at all if it uses use_lockfile instead.
Generates a repeated nested block (e.g. multiple ingress {} blocks inside a security group) from a list or map at runtime — the nested-block equivalent of for_each on a resource, needed because a resource’s own nested blocks can’t take for_each directly.
An ephemeral "..." {} resource or an ephemeral = true variable/output (Terraform 1.10+) exists only during a single plan/apply — never written to state or a plan file — the mechanism a short-lived credential or generated password can flow through without ever landing on disk.
A Terragrunt hook (inside terraform { error_hook "..." {} }) that runs only when the wrapped Terraform command fails, matched against the error output by on_errors regexes — used for cleanup or alerting that a plain before_hook/after_hook can’t express.
The current Terragrunt flag for narrowing which units a run --all targets (by path, dependency direction, or a saved query) — the replacement for the older, now-removed --terragrunt-include-dir/--terragrunt-exclude-dir pair.
An include block’s find_in_parent_folders() function walks upward from a unit’s directory to locate a named file (usually root.hcl) — the mechanism that lets every unit include the same root config without hardcoding a relative path that breaks when units move.
terragrunt find and terragrunt list discover units under the current tree (optionally as JSON, optionally filtered by dependency) — the read-only discovery commands behind --filter and behind editor/CI tooling that needs to enumerate units without running anything.
Turns a single resource/module block into one instance per entry of a map or set of strings, keyed by that map’s key (resource.name["prod"]) instead of a numeric index — removing an entry only affects that one instance, unlike count.
terraform force-unlock <lock-id> removes a stuck state lock by ID without running any plan/apply — the escape hatch for a lock left behind by a crashed process or a killed CI job, and the one command where using it on a lock that’s still legitimately held can corrupt state.
A Terragrunt generate "..." {} block writes a file (a provider block, a backend block, a version constraint) into a unit’s working directory before Terraform runs — the mechanism that lets every unit share identical provider/backend boilerplate defined once in root.hcl.
HashiCorp’s managed remote-execution platform (formerly Terraform Cloud): runs plan/apply on its own workers, stores state, and gates applies behind approvals and Sentinel policy — a managed alternative to self-hosting an S3 backend plus a CI pipeline.
The JSON policy attached to the IAM role a CI pipeline assumes should list only the actions/resources that pipeline’s applies actually need — a role broad enough to iam:*/*:* turns any compromised pipeline into full account compromise, the single most common production-checklist finding.
Inside lifecycle {}, ignore_changes = [tags] (or a specific list of attributes) tells Terraform to stop reporting — and stop reverting — drift on just those attributes, commonly used where another process (autoscaling, a tagging Lambda) legitimately owns them.
A declarative import { to = ..., id = ... } block (Terraform 1.5+) brings an existing resource under management by including it in a normal plan/apply, instead of the older imperative terraform import command — reviewable in a PR before anything actually changes.
A Terragrunt include "root" { path = find_in_parent_folders() } block pulls another .hcl file’s configuration into the current unit, merging its generate/remote_state/inputs — the primary DRY mechanism, alongside dependency, for a multi-unit tree.
Estimates the monthly cost delta a plan would introduce by matching planned resource changes against cloud pricing data — run against terraform show -json’s plan output, commonly posted as a PR comment alongside a policy-as-code gate.
The S3 backend’s kms_key_id argument (with encrypt = true) has S3 encrypt the state object with a customer-managed KMS key instead of the default SSE-S3 key — needed wherever state itself must meet a customer-managed-key compliance requirement.
The lifecycle {} meta-argument block available on any resource, holding create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by — the one place Terraform lets you override its default replace/destroy behavior per resource.
A locals {} block names an expression once so it can be reused across a module without repeating it — never itself an input or an output, purely an internal, computed value.
The record a backend writes before starting a state-modifying operation, preventing a second concurrent plan/apply from reading or writing the same state at the same time — S3’s native lock is a .tflock object created via a conditional (if-none-match) write.
Inside a Terragrunt dependency block, mock_outputs supplies fake values for the dependency’s outputs so commands like validate/plan can run even before the dependency has ever been applied — restricted by default to plan-only via mock_outputs_allowed_terraform_commands so a real apply can’t silently run against fake data.
A terraform test construct (1.7+) that substitutes a real provider with a mock returning either computed placeholder values or an explicit override — the mechanism that lets a test run plan/apply with zero real credentials and zero real API calls.
A reusable, self-contained group of .tf files taking variable inputs and exposing outputs — called from elsewhere with a module "name" { source = ... } block, the unit of composition and reuse above a single resource.
A declarative moved { from = ..., to = ... } block (Terraform 1.1+) records a resource/module rename or move in state as a refactor, not a destroy+create — reviewable in a plan, and safe to leave in the config indefinitely for anyone who applies from an older state.
The pre-1.4 way to attach provisioner/triggers behavior with no real infrastructure backing it — superseded by terraform_data, which does the same job as a first-class part of Terraform core instead of a separate provider dependency.
OpenID Connect: a CI pipeline exchanges its platform-issued, short-lived identity token for temporary cloud credentials via a trust relationship configured once — the mechanism that removes long-lived cloud access keys from CI secrets entirely.
Exposes a value from a module (or the root module, where terraform output reads it) to whatever called it — a Terragrunt dependency block reads a unit’s outputs exactly this way.
A terraform test construct that replaces one specific resource’s planned values (rather than an entire provider, like mock_provider) — used when only one resource in an otherwise-real plan needs a stand-in value.
terraform plan computes and shows the actions needed to reach the desired state without executing any of them — read-only against real infrastructure, though it does read current state and (unless -refresh=false) refresh it.
TF_PLUGIN_CACHE_DIR (or the CLI config’s plugin_cache_dir) points every terraform init at one shared directory of downloaded provider binaries instead of re-downloading the same provider version into every project’s own .terraform/providers — a meaningful speed and bandwidth win, but never safe for two concurrent inits to share unguarded.
A condition {} block’s precondition checks an assumption before a resource/data source/output is evaluated (e.g. “this AMI must be in the account”); postcondition checks the result after — both fail the plan/apply with a custom message the moment the expression is false, rather than surfacing a confusing downstream error.
The lifecycle { prevent_destroy = true } meta-argument makes Terraform refuse to plan a destroy of that resource at all — errors out the plan rather than silently protecting it, so a moved/removed refactor around it still needs care.
The plugin translating HCL resource/data-source blocks into real API calls against one system — aws, azurerm, google, and hundreds more, each versioned and installed independently of Terraform core itself.
A second (or third) configuration of the same provider, given a name via alias = "..." in a provider {} block, and referenced on a resource with provider = aws.east — how a single config manages resources across, e.g., two AWS regions in one apply.
The versioned RPC contract (protocol v6 since Terraform 1.4+/current providers) between Terraform core and any provider plugin — why a provider binary built against protocol 6 can’t be loaded by a CLI that only speaks protocol 5, independent of the provider’s own semantic version.
Re-reads every managed resource’s real state from the provider and reconciles it into Terraform’s state file — happens automatically as part of plan/apply unless -refresh=false, or explicitly via terraform apply -refresh-only.
The Terraform Registry (registry.terraform.io) hosts versioned, publicly discoverable providers and modules — a module source like terraform-aws-modules/vpc/aws resolves here by default, no full Git URL needed.
A Terragrunt remote_state { backend = "s3" ... } block generates the equivalent Terraform backend block for every unit that includes it, keeping bucket/key/region/lock config in exactly one place instead of copy-pasted per unit.
A declarative removed { from = ... } block (Terraform 1.7+) drops a resource out of state — optionally with a nested lifecycle { destroy = false/true } — without needing the resource’s config to still exist at all, the safe replacement for terraform state rm when the config is also being deleted.
terragrunt render prints a unit’s fully-resolved configuration (after include, generate, and function evaluation) as JSON or HCL — the way to see exactly what a unit will actually run without triggering plan/apply.
Inside lifecycle {}, replace_triggered_by = [aws_launch_template.this] forces this resource to be replaced whenever the referenced resource/attribute changes — even though nothing about this resource’s own arguments changed.
The core building block: a resource "aws_instance" "this" {} block declares one real, managed object whose full lifecycle (create/read/update/delete) Terraform owns end-to-end.
The .tf files in the directory you actually run terraform from — every other module is a child module reached through it, and it’s the only place a provider {} block or a backend configuration can live.
The conventional name (replacing the deprecated terragrunt.hcl root pattern) for the top-of-tree Terragrunt file every unit’s include block locates via find_in_parent_folders() — holds shared remote_state, generate, and provider config.
Runs a Terraform command across every unit under the current directory in dependency order — the current syntax is terragrunt run --all apply; the older per-command forms (terragrunt plan-all, terragrunt run-all apply) are unrecognized commands on Terragrunt 1.1.
The S3 backend’s use_lockfile = true argument locks state using a .tflock object and S3’s conditional-write (If-None-Match) support, introduced experimentally in Terraform 1.10 and stable since 1.11 — the replacement for a separate DynamoDB lock table.
Enabling versioning on the bucket backing an S3 backend keeps every prior state object as a recoverable version — the practical recovery path after a bad apply corrupts state, since Terraform itself keeps no separate state history beyond the local terraform.tfstate.backup of the last run.
aws_secretsmanager_secret/_version supports automatic rotation and resource policies but bills per secret; aws_ssm_parameter (SecureString type) is free up to a generous tier but has no built-in rotation — the practical reason a team picks one over the other for a given credential.
The write-only counterpart (Terraform 1.11+) of aws_secretsmanager_secret_version’s secret_string — its value is sent to AWS and never persisted in state or a plan file, paired with a secret_string_wo_version you bump to force an update.
Marking a variable/output with sensitive = true redacts its value from CLI output and plan/apply logs — it is still stored in plaintext in the state file itself, which is why sensitive state also needs backend-level encryption.
Policy-as-code engines that evaluate a plan’s show -json output against organization-written rules before an apply is allowed to proceed — Sentinel is HCP Terraform’s built-in engine; Open Policy Agent (via conftest) is the open-source, platform-agnostic equivalent.
terraform show -json <planfile> renders a saved plan (or state) as machine-readable JSON — what Sentinel/OPA, Infracost, and most custom CI gates actually parse, instead of scraping the human-readable plan output.
The JSON record (terraform.tfstate, or its remote-backend equivalent) mapping every config resource/module to the real object it manages — Terraform’s only source of truth for what it already created, never re-derived by scanning the cloud account.
Records the exact provider versions and their checksums (across every platform, unless pruned with -platform) that satisfied a terraform init — committed to version control so a second init on a different machine can’t silently resolve a different, unverified provider build.
Emits the dependency graph Terraform actually built for the current config, in DOT format, piped to Graphviz (terraform graph | dot -Tsvg) to visualize why one resource depends on another.
The built-in test runner (.tftest.hcl files, Terraform 1.6+) that plans and optionally applies a configuration against real or mocked providers and asserts on the result — run with terraform test, no separate testing framework required.
The file extension terraform test looks for: one or more run blocks, each with a command (plan or apply) and assert blocks checking conditions against that run’s planned or applied values.
A first-class, provider-independent resource (Terraform 1.4+) holding arbitrary input/triggers_replace values with no real infrastructure behind it — the replacement for the community null_resource, commonly used to force a dependent resource’s replacement or to carry provisioner behavior.
A data source reading another configuration’s state (and its outputs) directly out of its backend — the plain-Terraform equivalent of what a Terragrunt dependency block does through mock_outputs and generated wiring.
A terragrunt.stack.hcl file declares a set of unit/stack blocks that terragrunt stack generate expands into a real directory tree of units (under .terragrunt-stack/) before stack run applies them — a higher-level way to template “one stack of units” than copy-pasting directories per environment.
A linter for Terraform source that catches provider-specific mistakes plan/apply won’t (a deprecated instance type, an invalid AWS region string) that terraform validate — which only checks HCL syntax and internal consistency — cannot.
Terragrunt’s name for one directory containing one terragrunt.hcl plus the Terraform module it points at via terraform { source = ... } — the thing run --all, dependency, and --filter all operate on, analogous to a root module in plain Terraform.
A variable’s validation { condition = ..., error_message = ... } block rejects an invalid value at plan time with a custom message; since Terraform 1.9, the condition may also reference other variables, so cross-variable rules no longer need a workaround via precondition.
A named, typed input to a module or the root config, given a value via .tfvars, -var, an environment variable (TF_VAR_name), or a default — the only way data flows into a module from outside it.
The pessimistic-constraint operator: ~> 6.66 allows 6.66.x (patch upgrades only), while ~> 6.0 allows any 6.x (minor upgrades too) — the standard way to pin a provider loosely enough to get bugfixes but not a breaking major version.
Terraform’s built-in mechanism (terraform workspace new/select) for running one config against multiple named state files sharing the same backend — this course teaches Terragrunt’s directory-per-environment pattern instead, since workspaces silently share the same backend config and code across every environment, which is often the wrong tradeoff for prod vs. non-prod.
A variable/attribute marked write_only = true (Terraform 1.11+) is accepted on input but never stored in state or a plan file — paired with a _version argument you bump to signal “this write-only value changed, re-send it.”