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 GCP-specific pieces (the gcs backend’s generation-based locking, google vs google-beta, Secret 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.
The credential-discovery chain the google/google-beta providers use when no explicit credentials/service-account key is configured — a local gcloud auth application-default login token during development, or the attached service account automatically when running inside GCP/CI with Workload Identity Federation.
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.
The plugin that decides where Terraform’s state is stored and how operations against it are locked — configured once in a terraform { backend "gcs" {} } block; changing the block’s arguments requires re-running terraform init (with -migrate-state to move existing state).
Google Cloud’s managed CI/CD runner, configured via cloudbuild.yaml, wired to a google_cloudbuild_trigger resource — the GCP-native alternative to running Terraform’s plan/apply from GitHub Actions or another external CI system.
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 GCS buckets, unencrypted persistent disks, overly permissive firewall rules, …) — 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. google.us_west1, google.us_east1) 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 "google_compute_image" "..." {}) that fetches information about a resource Terraform doesn’t manage — either something that already exists (a shared VPC) or a provider computation (an image family matching filters) — and can never be created, updated, or destroyed by Terraform itself.
A resource-level attribute (DELETE/PREVENT/ABANDON) some Google resources expose to override the provider’s own default destroy behavior — google_project, for one, defaults its own deletion_policy to PREVENT, so a plan that would delete a project fails outright unless you explicitly set it to DELETE (or ABANDON, which drops it from state without touching the real project).
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 GCP — 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.
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.
The gcs backend’s equivalent of the S3 backend’s key: a path prefix inside the bucket under which the backend stores its state object (and, per Terragrunt unit, usually one prefix per unit rather than one bucket per unit).
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.
The google provider covers GA (generally available) resources and fields; google-beta is a separate provider (same resources, same schema) that additionally exposes beta/preview fields and resources — a resource using a beta-only field must be declared with provider = google-beta explicitly, it doesn’t fall back automatically.
Enables a specific Google Cloud API (e.g. compute.googleapis.com) on a project — a resource that depends on an API most other providers assume is “just on” needs this as an explicit prerequisite, or its first apply fails with an API-not-enabled error.
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 a gcs backend plus a CI pipeline.
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.
GCP’s resource-tagging mechanism (a flat map of key/value strings on most resources) — the direct equivalent of AWS/Azure “tags,” just under a different name and with its own, stricter key/value character-set rules.
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 — the gcs backend’s lock uses the state object’s generation number as an optimistic-concurrency precondition — built into the backend from the start, with no separate lock resource to provision, unlike the S3 backend’s history with a DynamoDB table.
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.
Most google resources take their own project/region/zone arguments, but silently fall back to the provider block’s defaults when omitted — and separately, a brand-new GCP project auto-creates a default VPC (auto_create_network on google_project, true by default) with an auto-mode subnet in every region unless explicitly disabled.
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 = google.us_west1 — how a single config manages resources across, e.g., two GCP 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-google-modules/network/google resolves here by default, no full Git URL needed.
A Terragrunt remote_state { backend = "gcs" ... } block generates the equivalent Terraform backend block for every unit that includes it, keeping bucket/prefix 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 = [google_compute_instance_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 "google_compute_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.
google_secret_manager_secret_version’s write-only counterpart (Terraform 1.11+) of secret_data — the value is sent to Secret Manager and never persisted in state or a plan file, paired with a secret_data_wo_version you bump to push a new value.
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 community VPC module’s major-version-18 breaking change: subnets (the list describing each subnet to create) became a required argument with no default — a v17-style call that omitted it and relied on auto-mode subnetting no longer plans at all until subnets is supplied explicitly.
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 machine type, an invalid GCP 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.
GCP’s federated-identity mechanism: a workload identity pool + provider trusts a CI platform’s OIDC issuer, letting a pipeline impersonate a service account via short-lived tokens — the standard replacement for downloading and storing a service-account JSON key as a CI secret.
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.”