SaveMyCert
Log in
5 of 5 free questions left today·for 10 a day
Infrastructure as Code (IaC) with Terraform

Infrastructure as Code with Terraform: Concepts and Multi-Cloud Workflows

15 min readTerraform 004 · Infrastructure as Code (IaC) with TerraformUpdated

Infrastructure as Code (IaC) is the practice of defining infrastructure — servers, networks, DNS records, databases, SaaS configuration — in machine-readable files that you version, review, and apply automatically, instead of clicking through consoles or running one-off commands. Terraform is HashiCorp's IaC tool: you describe the desired end state in HCL (HashiCorp Configuration Language), and Terraform works out what to create, change, or destroy to reach it. Because Terraform talks to every platform through plugins called providers, one workflow — write, plan, apply — manages AWS, Azure, Google Cloud, on-premises systems, and hundreds of other services. The Terraform Associate 004 exam opens with exactly this material: what IaC is, the advantages of IaC patterns, and how Terraform handles multi-cloud, hybrid-cloud, and service-agnostic workflows. This lesson covers all three sub-objectives, plus the declarative-versus-imperative distinction, how providers plug into Terraform, how to read a plan when reviewing a change, and how existing infrastructure is brought under Terraform.

What you’ll learn
  • Define Infrastructure as Code, explain how it differs from manual provisioning, and say what makes an object "managed" by Terraform
  • Explain the advantages of IaC patterns — versioning, repeatability, automation, reduced human error, and self-service — and the limits of each
  • Distinguish declarative configuration from imperative scripting, and predict what plan proposes when the configuration or reality changes
  • Read plan action symbols (+, ~, -/+, +/-, -, <=) to spot destructive changes in review
  • Explain how provider plugins, installed by terraform init, let one Terraform binary run one workflow across multi-cloud, hybrid-cloud, and service-agnostic estates
  • Describe how console-built or other-tool infrastructure is adopted with resource blocks plus import
  • Position Terraform against configuration management tools such as Ansible, Chef, and Puppet

What Infrastructure as Code is

Infrastructure as Code means you define infrastructure in machine-readable configuration files rather than building it by hand. The files are the source of truth: they describe what should exist, they live in version control alongside application code, and a tool — Terraform — reads them and makes reality match. Provisioning stops being a sequence of console clicks that nobody can reproduce and becomes an artifact you can review, test, and re-run.

Terraform configurations are written in HCL, a human-readable declarative language (JSON is also accepted). The core unit is the resource block, which declares one infrastructure object. This is a complete, valid resource:

resource "aws_instance" "web" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
  }
}

Read it as: an EC2 instance, known inside this configuration as aws_instance.web, with that AMI, that size, and that tag. Nothing here says how to create the instance — no API calls, no ordering, no retry logic. That is Terraform's job. You commit the file, run terraform plan to preview the change, and terraform apply to execute it.

Only objects declared as resources of a provider are managed: Terraform records them in state, compares them with reality on every plan, and changes or destroys them on apply. Three look-alikes are not management: a data source only reads an existing object; a provisioner (a local-exec or remote-exec command, whether on a null_resource or on its built-in successor terraform_data) runs a command once and keeps no record of what that command changed; and the inputs stored by terraform_data are just values Terraform tracks, not the external object a script touched. If you want something tracked in state and diffed in plan, it needs a provider resource type.

The definition to hold onto: IaC manages infrastructure through machine-readable definition files, giving infrastructure the same lifecycle as software — written, versioned, reviewed, and deployed.

The advantages of IaC patterns

IaC patterns deliver five advantages: versioning, repeatability, automation, reduced human error, and self-service. Each one is a direct consequence of infrastructure being code.

ConcernManual provisioningInfrastructure as Code
Change history and approvalThe provider's audit log shows which API calls were made, but not the intent, the review, or who approved themEvery change is a commit — diff, blame, revert — and the review and approval live in the pull request
Reproducing environmentsHand-rebuilt from memory or wiki notes; environments drift apartRe-apply the same configuration to get a consistent stack (dev, staging, prod)
SpeedLimited by how fast a human can clickRuns unattended in CI/CD pipelines; large stacks provision in minutes
Human errorTypos and skipped steps are invisible until something breaksChanges are previewed with a plan and peer-reviewed before apply
Access modelAn ops team is the bottleneck for every requestTeams self-serve from approved, reusable configurations and modules

Versioning: where the history and the approvals live

Version control is the audit trail for infrastructure intent: the commit history shows what the configuration looked like at any date, and the pull request shows who proposed, reviewed, and approved each change. Other artifacts do not replace it. The state file maps resources to real objects and their current attributes; it records no reviewer or approver, and a plain local state file keeps only the latest snapshot plus one backup of the previous one (terraform.tfstate.backup) — not a history. A saved plan describes one proposed run. The cloud provider's API audit log (for example AWS CloudTrail) records which identity made each API call — including calls made by hand in the console — but not why, and not who signed off.

Repeatability: same code, consistent environments

Repeatability rests on idempotency: applying the same configuration twice produces the same result, and a second apply with no changes does nothing. Feed one configuration different variables (Region, size, name prefix) and you get dev, staging, and prod that match in structure, or a rebuilt environment in another Region after a disaster. Be precise about what gets rebuilt: configuration recreates the infrastructure, not the data inside it. A recreated database is empty; restoring its contents still needs backups or snapshots. State holds resource metadata (IDs and attributes), never database rows or application files, and a refresh-only run (terraform plan -refresh-only / apply -refresh-only) only updates state to match what exists — it never creates anything.

Reduced human error: review the plan, not just the diff

Changes are proposed as code, previewed with terraform plan, and peer-reviewed before apply. The plan matters as much as the code diff: a one-line diff can look harmless while the provider must destroy and recreate the object to make it. Plan output marks each action with a symbol:

SymbolActionWhat happens to the existing object
+createNothing exists yet; a new object is created
~update in-placeSame object, modified; its data survives
-/+destroy and then create replacementOld object destroyed first, then a new one created; the plan notes which argument "forces replacement" and says the resource "must be replaced"
+/-create replacement and then destroyReplacement with create_before_destroy: the new object is created before the old one is destroyed — still a new object
-destroyObject deleted
<=readA data source is read during apply; nothing is managed or changed

For a stateful resource such as a database, -/+ or +/- is the line to catch in review: both mean a new, empty object.

Self-service: guardrails written into the module

A platform team publishes a reusable module, and product teams provision what they need by setting a few inputs — no tickets. The guardrails live in the module's code, where callers cannot skip them: settings that must never vary (encryption on, logging on) are hard-coded rather than exposed as variables, and inputs that may vary are checked with validation blocks on the variable (for example, the Region must be in an allowed list), so a bad value fails at plan. A wiki of rules, or console access with instructions, gives self-service without enforcement. (HCP Terraform adds organisation-wide policy checks; that is covered in the HCP Terraform lesson.)

Declarative versus imperative: how Terraform thinks

Terraform is declarative: you describe the desired end state, and Terraform computes the steps to reach it. An imperative approach — a bash script full of aws ec2 run-instances calls, for example — instead specifies the exact commands to run, in order, and it is your job to handle what already exists, what failed halfway, and what order dependencies require. Keeping a script in Git makes it versioned, but it is still imperative.

The difference shows up the second time you run something. Run an imperative creation script twice and you get duplicate servers, or an error, unless you wrote existence checks yourself. Run terraform apply twice and the second run reports no changes, because Terraform compares your configuration against refreshed state and only acts on the difference.

Terraform converges reality on the declaration in both directions:

  • Declaring more (count raised from 3 to 5) plans two creates.
  • Declaring less (count lowered, or a resource block deleted from the configuration) plans destroys for the objects no longer declared. Deleting a block does not "forget" the object; it removes it. (To stop managing an object without destroying it, Terraform 1.7+ offers a removed block with destroy = false — see the state lesson.)
  • Reality changes behind Terraform's back (someone deletes a managed VM in the console): a normal terraform plan refreshes state first, finds the object gone, and proposes creating it again — no separate refresh step is needed.

Declarative also means ordering is inferred. When one resource references another's attribute, Terraform builds a dependency graph and creates, updates, or destroys resources in the correct order — and parallelizes wherever the graph allows. Block order in the file carries no meaning. In an imperative script, you hand-author that ordering and it silently breaks when the topology changes.

Signals in wording: "describes the end state" and "what, not how" point at declarative; "step-by-step", "sequence of commands", and "specifies how" point at imperative. Terraform, CloudFormation, Bicep, and Kubernetes manifests are declarative; shell scripts and raw CLI or SDK calls are imperative.

One workflow, many providers: how Terraform manages multi-cloud

Terraform manages multi-cloud by separating the workflow from the platform. Terraform core knows how to parse configuration, build a dependency graph, plan, and apply — but it makes no cloud API calls itself and ships no platform-specific resource types. Everything platform-specific lives in providers, plugins that translate resource blocks into API calls: the aws provider knows EC2 and S3, the google provider knows Compute Engine, the azurerm provider knows Azure Resource Manager. Terraform does not shell out to each cloud's CLI.

Three facts about providers follow from the plugin model:

  • They are not compiled into Terraform. One Terraform binary serves every platform; there is no "AWS edition". terraform init reads the configuration's provider requirements, downloads the plugins (usually from the Terraform Registry) into the working directory, and records the selected versions in the dependency lock file. Even utility providers such as random and tls are separate plugins.
  • Only init installs them. Add a resource from a provider the directory has not used before and run terraform plan straight away, and plan stops with an "Inconsistent dependency lock file" error saying that provider is required but no version is selected, and tells you to run terraform init. Plan and apply never download providers.
  • They are versioned separately from core. Each provider has its own release cycle and version numbers, which is why configurations pin them with version constraints. Installing and versioning providers in detail belongs to the providers lesson.

Because of that split, resources from different clouds can sit in one configuration and flow through one plan, one apply, and one state:

resource "aws_instance" "api" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
}

resource "google_compute_instance" "worker" {
  name         = "worker-1"
  machine_type = "e2-small"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }

  network_interface {
    network = "default"
  }
}

Same language, same commands, same state management — one team skill set covers every platform. That is the operational win of multi-cloud Terraform: you avoid one tool and one workflow per cloud (CloudFormation for AWS, ARM templates or Bicep for Azure), and fewer tools means fewer processes to keep consistent. It does not, by itself, isolate failures between clouds or bypass any cloud's permissions — those come from architecture and IAM.

The limit: the workflow is portable, the configuration is not

Terraform makes the workflow cloud-agnostic, not the configuration. There is no generic virtual_machine type that deploys wherever you point it: an aws_instance only ever means an EC2 instance, and changing the provider block converts nothing. Moving a workload to Azure means rewriting its resources with azurerm types; init, plan, and apply stay the same.

Hybrid cloud and service-agnostic workflows

Hybrid cloud in Terraform terms means one configuration and one workflow spanning public cloud and on-premises infrastructure. The provider model makes this uneventful: the vsphere provider manages VMware VMs in your data center by calling the vCenter Server API, while the aws provider manages the cloud half, and both land in the same dependency graph, the same plan output, and the same state — separate state per provider is not required. Terraform is agentless: it runs wherever you run it (a laptop, a CI runner, HCP Terraform or an agent you host for private networks) and talks to platform APIs; nothing is installed on ESXi hosts or on the VMs it creates.

Because the graph spans providers, one resource can feed another across the cloud boundary. Here a Route 53 record in AWS always carries the on-premises VM's current IP address:

resource "vsphere_virtual_machine" "db" {
  # ... name, resource pool, datastore, network, disk ...
}

resource "aws_route53_record" "db" {
  zone_id = var.zone_id
  name    = "db.internal.example.com"
  type    = "A"
  ttl     = 300
  records = [vsphere_virtual_machine.db.default_ip_address]
}

The reference makes Terraform create the VM first and the record second, and if the VM is replaced with a new IP, the same plan updates the record. Two separate configurations with a hand-copied IP would need two plans and a manual step that lets the record drift.

Service-agnostic pushes the idea past infrastructure entirely. A provider is just a bridge to an API, so Terraform manages anything with a provider: Kubernetes objects, GitHub repositories and teams, Datadog monitors, Cloudflare DNS, PagerDuty schedules, Vault policies. The Terraform Registry hosts thousands of providers. If a SaaS tool has an API but no provider, a provisioner calling that API is not a substitute — the objects would not be in state and drift would be invisible to plan.

Utility providers such as random and tls call no external API at all, yet their resources follow the normal resource lifecycle: a random_id, random_password, or tls_private_key is generated once on create, stored in state, and keeps the same value on every later plan and apply until the resource is replaced (for example, when one of its arguments or keepers changes). That is what makes them safe for naming:

resource "random_pet" "suffix" {
  length = 2
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "artifacts-${random_pet.suffix.id}"
}

The bucket name interpolates an attribute of the random resource, so Terraform creates the pet name first, then the bucket — and the name does not change on the next run. (Because state stores these values, a generated password sits in state in plain text; protect state accordingly.)

Map the three phrases to one mechanism: multi-cloud (several public clouds), hybrid cloud (cloud plus on-premises), and service-agnostic (any API — DNS, monitoring, source control) all work because Terraform core is platform-neutral and providers carry all platform knowledge.

Adopting Terraform for infrastructure that already exists

Infrastructure built by hand in a console, or by another IaC tool such as CloudFormation or Bicep, can be brought under Terraform without rebuilding it. Console-created resources are not lost to IaC; the problem with manual provisioning is that nothing records how they were built, so reproducing them reliably is hard — not that they can never be managed.

Adoption has two parts, and both are required:

  1. Write a resource block in HCL for each object, using the right provider's resource type.
  2. Import the running object into state against that block — with an import block (Terraform 1.5+, planned and applied like any other change) or the terraform import command.

Skip the import and Terraform has no record of the object, so the new block is planned as a create: the first apply builds a second copy (or fails on a name clash) instead of adopting the running one. Every imported object needs a matching resource block; for objects named in import blocks, terraform plan -generate-config-out=generated.tf drafts those blocks for you to review and tidy. The import mechanics are covered in detail in the import and state-inspection lesson.

Terraform versus configuration management tools

Terraform is a provisioning tool: it creates, modifies, and destroys infrastructure — the servers, networks, load balancers, and managed services themselves. Configuration management tools such as Ansible, Chef, Puppet, and SaltStack work one layer up: they install packages, edit files, and manage services on machines that already exist. Each tool belongs on its own layer; neither replaces the other.

The philosophical split is mutability. Configuration management leans mutable: the same long-lived server is updated in place, patch after patch, which risks configuration drift between supposedly identical machines. Terraform leans immutable: when a change cannot be made in place, the provider replaces the resource — destroys the old one and creates a new one from the current definition. Changing an instance's image is the classic case: update the AMI ID in configuration and the plan shows each server as -/+ "must be replaced", because an instance's boot image cannot be swapped in place. Every server is born from code, so drift has nowhere to accumulate.

The tools are complementary, and real pipelines chain them: Terraform provisions the VM, then Ansible configures the software on it (or the software is baked into a pre-built machine image that Terraform launches). The distinction that matters is what they manage, not how they express it: Terraform and Puppet are declarative, Chef recipes and Ansible playbooks are written more procedurally, and all of them aim for idempotent runs.

Signals in wording: "provisions infrastructure lifecycle", "replaces servers", and "immutable" point at Terraform; "installs software on existing servers", "in-place", and "mutable" point at configuration management.

Common misconceptions: keeping the concepts straight

Most mistakes in this area are near-miss statements — each one almost true, wrong in one word:

  • "Terraform configurations are portable across clouds." False. The workflow is cloud-agnostic; resource blocks are provider-specific.
  • "Terraform is imperative because you run commands like apply." False. Running a CLI does not make a tool imperative — the configuration declares end state, and Terraform derives the steps.
  • "IaC eliminates human error." Too strong. IaC reduces error through previewed plans, peer review, and repeatable runs.
  • "Terraform replaces Ansible." False. Provisioning and configuration management are different layers.
  • "Multi-cloud requires separate state or separate tools per cloud." False — one configuration can hold multiple providers, one plan and one state cover them all, and cross-provider references work.
  • "The state file is an audit trail." False. State holds current resource metadata; approvals live in pull requests, and API calls are logged by the provider.
  • "Terraform rebuilds everything after a disaster, data included." False. It recreates infrastructure from configuration; data needs backups.
  • "Console-built resources can never be managed by Terraform." False. Write resource blocks and import them.
  • "A provisioner or data source makes an object managed." False. Only a provider's resource types are tracked in state and diffed in plan.

Keep the advantage list ready and map symptoms to it: environments drifting apart between dev and prod → repeatability; "what changed and who approved it" → versioning; a bottlenecked ops team → self-service; a risky change caught before it ran → reduced human error through plan review.

Tip. Expect definition and best-description questions: what IaC is, which IaC advantage solves a described symptom (drift between environments, missing change history and approvals, ticket bottlenecks, risky changes reaching production), and whether an approach is declarative or imperative. Behaviour questions ask what plan proposes when nothing changed, when a block is removed or count lowered, or when someone deletes a managed resource by hand, and which plan symbol signals a replacement. Provider questions probe the plugin model: core makes no API calls, init installs providers, providers are versioned separately, and only provider resources are managed. Multi-cloud and hybrid questions test one configuration and state across providers, cross-provider references and the agentless model — and the workflow-versus-configuration distinction, where wrong answers claim resource blocks port between clouds. Also expect adopting existing infrastructure (resource blocks plus import), the limits of IaC (data still needs backups; state is not an audit trail) and Terraform versus configuration management tools.

Key takeaways
  • IaC defines infrastructure in machine-readable, version-controlled files — the definition files are the source of truth, not the console
  • The advantages of IaC patterns: versioning, repeatability, automation, reduced human error, and self-service
  • Approvals live in pull requests; state holds current resource metadata and the provider audit log records API calls — neither records who approved a change
  • Configuration rebuilds infrastructure, not the data inside it — databases still need backups
  • Terraform is declarative: it converges on the declared state — re-applying unchanged code does nothing, removing a block or lowering count plans destroys, and a manually deleted resource is planned as a create
  • In plan output, ~ is an in-place update while -/+ and +/- are replacements that produce a new object — the line to catch for stateful resources
  • Providers are separately versioned plugins installed only by terraform init; Terraform core makes no API calls, and one binary serves every platform
  • One configuration, plan and state can span clouds, on-premises (vSphere, agentless) and SaaS APIs, with cross-provider references — but resource types are provider-specific and do not port between clouds
  • Only provider resources are managed; provisioners and data sources are not. Existing resources are adopted by writing resource blocks and importing them
  • Terraform provisions infrastructure lifecycle (immutable, replace-on-change, e.g. a new image); Ansible, Chef, and Puppet manage software on existing servers — complementary, not competing

Frequently asked questions

What is Infrastructure as Code in simple terms?

Infrastructure as Code means defining your servers, networks, and services in configuration files instead of creating them manually in a console. A tool like Terraform reads the files and makes real infrastructure match them. Because the files are code, you can version them in Git, review changes before they happen, and re-run them to rebuild consistent environments.

Is Terraform declarative or imperative?

Terraform is declarative. You write configuration that describes the desired end state — five instances, one bucket, these tags — and Terraform compares it with refreshed state, computes the difference, and executes only the changes needed, in dependency order. Applying unchanged configuration again does nothing, and removing a resource from the configuration plans its destruction. An imperative approach, like a shell script of CLI commands, runs every command as written each time.

Can Terraform manage more than one cloud at the same time?

Yes. A single Terraform configuration can declare resources from multiple providers — AWS, Azure, Google Cloud, plus on-premises platforms like vSphere — and one terraform plan, apply and state cover them all. Terraform core is platform-neutral; each provider plugin, installed by terraform init, translates its resource blocks into that platform's API calls, and resources can reference attributes across providers.

Does multi-cloud Terraform mean I can move the same configuration from AWS to Azure?

No. Terraform makes the workflow portable, not the configuration. Resource types are provider-specific — an aws_instance is an EC2 instance and cannot become an Azure VM, and changing the provider block converts nothing. Moving to another cloud means rewriting resources with that cloud's provider (for example azurerm_linux_virtual_machine), while init, plan and apply stay the same.

Why does terraform plan fail after I add a resource from a new provider?

Providers are plugins that only terraform init installs and records in the dependency lock file. If you add a resource from a provider the working directory has not used before and run plan straight away, plan stops with an inconsistent dependency lock file error saying no version of that provider is selected. Run terraform init again, then plan.

Can resources created by hand in the console be managed by Terraform later?

Yes. Write a resource block for each object and import the running object into state with an import block (Terraform 1.5 and later) or the terraform import command; plan -generate-config-out can draft the blocks for objects named in import blocks. Without the import, Terraform plans the new block as a create and would build a duplicate.

Does Terraform back up my data if I need to rebuild after a disaster?

No. Terraform configuration lets you recreate the infrastructure — networks, instances, empty databases — in a consistent way, for example in another Region. Terraform state holds resource IDs and attributes, not database rows or files, so the data inside your resources still has to come from backups or snapshots.

What is the difference between Terraform and Ansible?

Terraform is a provisioning tool: it creates, changes, and destroys infrastructure itself — VMs, networks, managed services — and favors immutable infrastructure, replacing resources such as a server whose image changes rather than patching them. Ansible is a configuration management tool: it installs packages, edits files, and manages services on machines that already exist, typically in place. Teams commonly use both: Terraform provisions the server, Ansible configures the software on it.

Source

This lesson covers the "Infrastructure as Code (IaC) with Terraform" domain of the official Terraform 004 exam guide. Vendors revise their guides — check the source for the current version.

Test yourself on this topic
Terraform 004 practice questions, with full explanations.
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?