---
title: "Terraform & Infrastructure as Code"
description: "Terraform and infrastructure-as-code terminology: providers and resources, the state file and remote backends, drift and refresh, modules and for_each, the plan and apply workflow, and the concepts underneath it all."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/glossary/post/terraform-and-iac
---

# Terraform & Infrastructure as Code

Terraform stops being confusing once you notice it compares three things rather than two: your configuration, its state file, and the real infrastructure. Nearly every plan output that looks wrong is one specific pair of those disagreeing, and knowing which pair tells you what to do about it.

_Configuration, state, and reality, and which command touches which_

State is the part worth understanding properly, because it holds identity. It's what knows that `aws_instance.web` in your code means `i-0abc123` in AWS. Lose it and Terraform can't connect your configuration to anything, so it plans to build all of it again while the real resources sit there unmanaged.

That's also why the lock matters more than it looks like it should. Two people applying at once, both starting from the same state, means one write wins and whatever the other one created becomes an orphan: still running, still billing, invisible to Terraform forever.

The terms below are grouped the way the work splits. Concepts first, then the building blocks you write, then state, then the reuse mechanisms, then the commands.

## Terms

### Infrastructure as code (IaC) (Concepts)

Managing infrastructure by describing it in files that are version-controlled, reviewed and applied by a tool, rather than by clicking in a console or running ad-hoc commands. The value is not automation on its own, it is that the current intended shape of your infrastructure is a readable artifact with a history.

**Example:** A pull request that adds three lines to a .tf file and shows a plan output is a reviewable infrastructure change. A console click is not.

### Declarative (Concepts)

You describe the desired end state and the tool works out the steps. Terraform is declarative. The opposite, imperative, means you write the steps and the end state is whatever they produce.

**Example:** Declarative: "there should be three instances". Imperative: "create an instance, three times". The declarative version behaves correctly when there are already two.

### Idempotency (Concepts)

Running the same operation repeatedly produces the same result as running it once. In Terraform this is why `apply` on an unchanged configuration reports no changes rather than creating duplicates. It is a property of the whole model, not a feature you enable.

**Example:** `terraform apply` twice in a row gives "No changes. Your infrastructure matches the configuration." A shell script calling `aws ec2 run-instances` twice gives you two instances.

### Immutable infrastructure (Concepts)

Replacing a resource rather than modifying it in place. Instead of patching a running server, you build a new image and launch a new instance from it. Terraform supports this naturally, since many attribute changes force a replacement anyway.

**Example:** Changing an EC2 `ami` forces a new instance. Changing a `tag` updates in place. The plan output tells you which, with the difference between `~` and `-/+`.

### Configuration drift (State)

Reality no longer matches what state records. Almost always caused by a change made outside Terraform: a console edit, another tool, or a service modifying its own resource. Terraform detects it during refresh and plans to bring reality back in line with the configuration.

**Example:** Someone widens a security group in the console to debug something. Your next plan shows a change you did not write, and applying it closes the hole again.

### Resource (Building blocks)

One managed infrastructure object. A resource block names a type and a local name, and together those form its address, which is how Terraform tracks it in state. The address is the identity, so renaming a resource block makes Terraform plan to destroy and recreate it.

**Example:** `resource "aws_s3_bucket" "logs" {}` has the address `aws_s3_bucket.logs`. Renaming `logs` to `log_bucket` destroys and recreates the bucket unless you use a moved block.

### Provider (Building blocks)

A plugin that knows how to talk to one API. The AWS provider translates Terraform's create, read, update and delete operations into AWS API calls. Providers are versioned separately from Terraform and are downloaded during init.

**Example:** You can use the aws, kubernetes, github and cloudflare providers in one configuration, and manage a DNS record and the load balancer it points at together.

### Data source (Building blocks)

A read-only lookup of something Terraform does not manage. It gives you attributes of an existing object without taking ownership of it, so destroying your configuration never destroys what a data source read.

**Example:** `data "aws_ami" "ubuntu"` finds the current AMI ID to launch from. It queries on every plan, which is also why a data source can make plans non-deterministic.

### Variable (Building blocks)

A named input to a module or root configuration, with an optional type, default and validation rule. Variables are how the same code serves dev and prod without being copied.

**Example:** `variable "instance_count" { type = number, default = 2 }`, then `-var instance_count=6` or a `prod.tfvars` file at apply time.

### Output (Building blocks)

A value a module publishes to its caller, or that the root configuration prints after apply. Outputs are the module's public interface: anything not exposed as an output is not reachable from outside.

**Example:** A vpc module outputs `private_subnet_ids`, and the calling configuration passes that into an eks module. Without the output, the caller cannot see the subnets at all.

### Local value (Building blocks)

A named expression computed inside a configuration. Unlike a variable it takes no external input, so it is purely a way to name something you use more than once and keep the logic in one place.

**Example:** `locals { name_prefix = "${var.project}-${var.env}" }`, then referenced as `local.name_prefix` in twenty resource names.

### Argument versus attribute (Building blocks)

An argument is something you set on a resource. An attribute is something you read from it, including computed values that only exist after creation. Many names are both, which is why the distinction only becomes obvious when you try to read something before it exists.

**Example:** You set `cidr_block` on a VPC (argument) and read `id` from it (attribute, computed). Referencing `id` before apply is what makes a value "known after apply" in the plan.

### Expression (Building blocks)

Any value computation in HCL, from a literal to a reference to a conditional to a function call. Terraform's language is expression-oriented rather than procedural, so there are no statements, loops or assignments in the usual sense.

**Example:** `var.env == "prod" ? "m5.large" : "t3.micro"` is a conditional expression, and it is how you branch without an if statement.

### State file (State)

A JSON document mapping each resource address in your configuration to the real object it manages, plus every attribute Terraform last observed. It is what lets Terraform know that `aws_instance.web` means `i-0abc123`, and losing it means Terraform can no longer connect your code to anything.

**Example:** Delete terraform.tfstate and the next plan proposes creating everything from scratch, while the real resources sit there untouched and unmanaged.

### Remote backend (State)

Storage for state outside the working directory, usually an S3 bucket, plus locking. It is what makes Terraform usable by more than one person, and it also keeps state off laptops and out of git.

**Example:** S3 with `use_lockfile = true` for native locking, or Terraform Cloud. On older configurations you will see a separate `dynamodb_table` for the lock.

### State lock (State)

A mutual exclusion held for the duration of an operation that writes state, so two people cannot apply at once. Without it, concurrent applies each write their own view of state and one of them silently loses resources.

**Example:** "Error acquiring the state lock" means someone else is applying, or a previous run died holding it. `terraform force-unlock` exists and should be used carefully.

### Refresh (State)

Reading the current attributes of every managed resource from the provider and updating state to match, without changing anything real. Terraform does this at the start of a plan by default, which is how drift is detected.

**Example:** `-refresh=false` skips it, which speeds up a plan on a large configuration at the cost of not noticing drift. `terraform plan -refresh-only` shows drift alone.

### Orphaned resource (State)

A real object that no state file references. It keeps existing, keeps costing money, and no Terraform run will ever touch it. Usually the result of a lost or overwritten state file, or a `state rm` without a follow-up.

**Example:** Two engineers apply at once without a lock. Both create a NAT gateway, one state write wins, and the other NAT gateway bills you for years.

### terraform import (State)

Bringing an existing real resource under management by writing it into state. It changes only state, never reality. You still need a matching resource block in your configuration, or the next plan will propose destroying what you just imported.

**Example:** `terraform import aws_s3_bucket.logs my-existing-bucket`, or the newer declarative `import` block which works inside a plan and can be reviewed.

### terraform state rm (State)

Removing a resource from state without destroying it. Terraform forgets the resource exists; the real object survives untouched. The counterpart to import, and the correct tool when handing a resource over to another configuration.

**Example:** Splitting one big configuration into two. `state rm` in the old one, `import` in the new one, and nothing is ever destroyed.

### moved block (State)

A configuration block declaring that a resource used to have a different address, so a rename or a move into a module is treated as a rename rather than a destroy and create. It replaced the manual `terraform state mv` for most refactors.

**Example:** `moved { from = aws_instance.web, to = module.app.aws_instance.web }` lets you restructure code without touching infrastructure.

### Sensitive value (State)

A value marked so Terraform redacts it from plan and apply output. Note carefully that this only affects display: the value is still stored in state in plaintext, so state itself must be treated as a secret.

**Example:** An RDS `password` marked sensitive shows as `(sensitive value)` in the plan, and sits readable in the state file in your S3 bucket.

### Module (Reuse)

A directory of Terraform files used as a reusable unit, taking variables in and returning outputs. Every configuration is already a module, the root module. A child module is one you call with a `module` block.

**Example:** A `vpc` module taking `cidr` and `az_count`, returning subnet IDs. Called three times for dev, staging and prod with different inputs.

### Root module (Reuse)

The directory you run Terraform in. It is the entry point, it owns the backend configuration and the provider configuration, and it is the only module that reads .tfvars files or environment variables.

**Example:** Child modules cannot configure a backend or receive `-var` flags. Everything reaching a child module is passed explicitly through its `module` block.

### Registry (Reuse)

A catalogue of publishable modules and providers, either the public Terraform Registry or a private one. Source a module by registry address and pin its version, the same way you would a library dependency.

**Example:** `source = "terraform-aws-modules/vpc/aws"` with `version = "~> 5.0"`. Without the version pin, a new release changes your infrastructure on the next init.

### count (Reuse)

Creating multiple instances of a resource, addressed by numeric index. The problem is that the index is positional, so removing an item renumbers everything after it and Terraform plans to destroy and recreate resources you never touched.

**Example:** Three buckets with `count = 3` are `[0]`, `[1]`, `[2]`. Remove the middle one from a list and `[2]` becomes `[1]`, so Terraform recreates it.

### for_each (Reuse)

Creating multiple instances addressed by a map key or set value rather than by position. Removing one entry affects only that entry. This is the reason to prefer for_each over count for anything that is a collection of distinct things.

**Example:** `for_each = toset(["logs", "backups", "media"])` gives addresses like `aws_s3_bucket.this["logs"]`, which are stable no matter what else changes.

### Dynamic block (Reuse)

Generating repeated nested blocks inside a resource from a collection. Needed because a nested block is not an argument, so you cannot simply assign a list to it.

**Example:** Generating one `ingress` block per port in a list. Readable up to a point; past two nested dynamic blocks a purpose-built module is usually clearer.

### Meta-argument (Reuse)

An argument Terraform itself understands on any resource, rather than one defined by the provider. The set is `count`, `for_each`, `provider`, `depends_on`, `lifecycle` and `providers`.

**Example:** You can use `for_each` on any resource type in any provider, because the provider is not involved in interpreting it.

### depends_on (Reuse)

An explicit dependency for a relationship that exists in reality but does not appear in any expression. It is a last resort, because Terraform already infers dependencies from references, and reaching for it usually means a missing reference.

**Example:** A null_resource running a script that needs an IAM policy attached, where the script never references the policy's attributes. Genuinely needs depends_on.

### lifecycle block (Reuse)

Per-resource overrides of Terraform's default behaviour. `create_before_destroy` for zero-downtime replacement, `prevent_destroy` as a guard, and `ignore_changes` for attributes something else legitimately manages.

**Example:** `ignore_changes = [desired_count]` on an ECS service whose replica count is set by autoscaling, so Terraform stops fighting the autoscaler.

### terraform init (Workflow)

Preparing a working directory. Downloads providers and modules, configures the backend, and writes the dependency lock file. It touches neither state contents nor real infrastructure, and it must be rerun whenever providers or module sources change.

**Example:** `terraform init -upgrade` re-resolves provider versions within your constraints and updates the lock file. Plain `init` respects the existing lock.

### terraform plan (Workflow)

Refreshing state, then computing the difference between configuration and state, and printing what it would do. It is read-only with respect to your infrastructure, which makes it the right thing to run in a pull request.

**Example:** `terraform plan -out=tfplan` saves the plan so apply executes exactly that, with no re-evaluation. This is what a CI pipeline should do.

### terraform apply (Workflow)

Executing a plan. It takes the state lock, calls provider APIs in dependency order, and writes the resulting state. If it fails partway, some resources exist and state records them, so rerunning continues rather than starting over.

**Example:** `terraform apply tfplan` applies a saved plan without prompting, which is how you guarantee CI applies what was reviewed.

### terraform destroy (Workflow)

Planning and applying the removal of everything in state. It is `apply` with every resource marked for destruction, in reverse dependency order.

**Example:** `terraform destroy -target=aws_instance.web` destroys one resource, though targeting is a debugging tool rather than a workflow.

### terraform validate (Workflow)

Checking syntax and internal consistency without contacting any provider API or reading state. Fast enough to run on every commit, and it catches type errors, unknown attributes and undeclared variables.

**Example:** Runs offline after init. It will not catch an invalid AMI ID, because verifying that requires an API call.

### terraform fmt (Workflow)

Rewriting configuration files to canonical style. Purely cosmetic, and worth enforcing in CI with `-check` so formatting never appears in a diff alongside a real change.

**Example:** `terraform fmt -recursive -check` exits non-zero when something is unformatted, which is the right CI behaviour.

### Plan output symbols (Workflow)

The prefix on each line of a plan tells you the action. `+` create, `-` destroy, `~` update in place, `-/+` destroy then create, `+/-` create then destroy (with create_before_destroy), and `<=` a read from a data source.

**Example:** Seeing `-/+` next to a database is the moment to stop and read the "forces replacement" annotation before typing yes.

### Provisioner (Workflow)

A block running a script during create or destroy. Officially a last resort, because it breaks the declarative model: a failed provisioner marks the resource tainted, and Terraform has no way to know what the script did.

**Example:** Prefer `user_data`, a configuration management tool, or a pre-baked image. `local-exec` for a one-off notification is the least objectionable use.

### Workspace (Workflow)

Multiple named states within one backend and one configuration. Useful for short-lived parallel copies of the same thing, and a poor fit for dev versus prod, because the environments then share provider configuration and cannot diverge.

**Example:** `terraform workspace new feature-x` for a temporary stack. For real environments, separate directories with separate backends are the widely recommended pattern.

### Dependency lock file (Workflow)

`.terraform.lock.hcl`, recording the exact provider versions and their checksums. It belongs in version control, so every machine and every CI run resolves the same providers.

**Example:** Commit it. A plan that differs between your laptop and CI is frequently a provider version difference this file would have prevented.

### Terraform Registry version constraint (Workflow)

The operators controlling which versions are acceptable. `~> 5.1` allows 5.1.x but not 5.2. `>= 5.0, < 6.0` allows any 5.x. An unpinned dependency means an unrelated `init` can change your infrastructure.

**Example:** Pin providers with `~>` on the minor version, and pin modules exactly. A provider major version bump is a migration, not an upgrade.
