Network Automation with Terraform
Declarative HCL and the core loop
A network engineer about to push a VLAN change to a live Catalyst switch wants one thing first: to see exactly what will happen before anything reaches the device. Terraform is built around that instinct, and it starts from a single idea. You write the end state you want in HashiCorp Configuration Language (HCL) and let Terraform work out the steps.
resource "iosxe_vlan" "core" {
vlan_id = 10
name = "core"
}
That block names a desired end state: VLAN 10, called core, should exist on the device. (iosxe_vlan is a resource from Cisco's IOS-XE provider, the subject of the final section; here it is just a concrete example.) You never tell Terraform to open a session, check whether the VLAN already exists, and add it only if it is missing. You state the end and Terraform's core computes the create, update, or destroy actions that reach it, in dependency order. That is what declarative[1] means, and it is the one break from an imperative script, where every step, its check, and its ordering are your job. Because the configuration is the desired state, re-running it unchanged is an idempotent no-op: Terraform sees reality already matches and does nothing.
The loop: init, plan, apply, destroy
Terraform runs the same short loop every time, with a review step wired into the middle. The core workflow[2] is write, then plan, then apply; terraform init is the setup that makes plan and apply possible, and terraform destroy is the teardown.
terraform init # install providers, set up the backend
terraform plan # preview the changes
terraform apply # make them real
terraform init reads your configuration and installs everything the run needs: it downloads the providers declared in the required_providers block, records the exact versions it chose in the .terraform.lock.hcl lock file, initializes the backend that stores state, and fetches any module sources. The init documentation[3] notes the command is always safe to run again, and you must re-run it whenever you add a provider, add a module, or change the backend — a plan that skips init after adding a provider fails because the plugin is not present yet.
Desired state versus recorded state
Two inputs drive every plan. Your HCL is the desired state; state is Terraform's own record of what it has already built, a map from each resource address to a real object. terraform plan refreshes state against the live provider, diffs it against your configuration, and reports the actions needed to converge, which the plan reference[4] calls the execution plan; terraform apply then carries them out. Where state lives and how a team shares it safely is its own subject, two sections on. For now, treat state as Terraform's memory of the last apply.
Reading a plan and committing it safely
A -/+ next to a live resource is the symbol to catch before you approve anything: it means Terraform will destroy that resource and build a new one, which on a live interface or VLAN is a brief outage, not a quiet edit. Every plan prints a symbol in front of each resource so you can read the blast radius at a glance.
| Symbol | Action | Effect on the device |
|---|---|---|
+ | Create | New resource added |
- | Destroy | Resource removed |
~ | Update in place | Attribute changed, no rebuild |
-/+ | Replace (destroy then create) | Rebuilt; brief outage possible |
+/- | Replace (create then destroy) | New built before old removed |
Terraform prints this legend at the top of every plan, and the create-a-plan tutorial[5] walks the same set.
Why a change becomes a replacement
Terraform updates a resource in place whenever it can, and falls back to destroy-and-recreate only when a changed argument cannot be modified on the existing object through the provider's API — one of the four actions the resource-behavior docs[6] list. Those are the immutable attributes: change one and the plan flips from a safe ~ to a -/+, and the line is annotated # forces replacement. The two replace symbols differ only in order — -/+ destroys first, leaving a gap where the resource is absent, while +/- creates the replacement first. You get +/- by setting the create_before_destroy lifecycle argument, covered with the rest of the lifecycle block later.
Apply exactly what you reviewed: plan -out
By default terraform apply re-runs planning and asks you to type yes. That is fine at a keyboard, but in a pipeline the configuration or live state can shift between the review and the apply. terraform plan -out=plan.tfplan writes the computed plan to a file, and terraform apply plan.tfplan runs exactly those actions with no re-planning and no prompt, as the plan reference[4] describes.
terraform plan -out=change.tfplan # compute and save the exact actions
terraform apply change.tfplan # run them verbatim, no re-plan, no prompt
This is the standard CI/CD shape: one stage plans and saves the artifact for approval, a later stage applies it. One caution the docs are explicit about — a saved plan records its planned values, including sensitive ones, in cleartext, so treat *.tfplan as a secret and never commit it.
Scope the blast radius: destroy and -target
terraform destroy removes every resource in state, which is exactly why it is a teardown command and not a way to delete one thing. To drop a single resource, delete its block from the configuration and run apply; Terraform then plans a - for that resource alone. When you genuinely must act on one resource in isolation, -target scopes a plan, apply, or destroy to a resource address plus its dependencies, where an address is <type>.<name> extended with module.<name> and a ["key"] or [index] suffix, per the resource-addressing reference[7].
terraform apply -target=iosxe_interface.gig2 # this resource + its dependencies
The plan reference is blunt[4]: targeting is for exceptional recovery, not routine operations, because everything outside the target is skipped and its drift goes unreconciled. Reach for -target to recover one broken resource; do not build a workflow on it.
State: the authoritative map and remote backends
State is authoritative, and that single fact drives everything about how a team must handle it. When you run apply, Terraform writes terraform.tfstate: a record tying every resource address in your HCL, such as iosxe_vlan.core, to the real object's ID. On the next run it reads that map, refreshes it, and diffs it against your configuration to decide what to change. The state documentation[8] treats this file as the source of truth for the infrastructure Terraform manages.
Why local state breaks a team
Because state is the source of truth, a state file that lives only on one engineer's laptop is invisible to everyone else. A teammate who clones the repository and runs apply starts from an empty map: Terraform finds no record of your resources, concludes they do not exist, and proposes to create them again — or, with a slightly different variable, to destroy and recreate live infrastructure. Nothing warns them, because from their state's point of view the world really is empty. The fix is not a command or a flag; it is to stop keeping state on one machine.
Remote backends and state locking
A backend decides where Terraform keeps state. The default local backend writes terraform.tfstate next to your code; a remote backend moves it to shared storage so every engineer and pipeline reads and writes one authoritative copy. The three you meet on the 350-901 exam are Amazon S3, Google Cloud Storage (GCS), and HCP Terraform (formerly Terraform Cloud).
Shared storage fixes visibility but opens a race: two people running apply at once, each overwriting the other's state. State locking closes it. When the backend supports it, Terraform takes a lock for any operation that could write state and releases it when the operation finishes, so a second apply waits its turn instead of interleaving, as the state-locking docs[9] describe. That shared, locked backend is the fix for the concurrency problem; running terraform refresh or swapping -var files does not touch the root cause.
| Property | Amazon S3 | Google Cloud Storage | HCP Terraform |
|---|---|---|---|
| Locking | Native S3 lockfile (use_lockfile) | Native, automatic | Built-in, automatic |
| Encryption at rest | Server-side (SSE-S3 / SSE-KMS) | Google-managed or CMEK | Managed, optional own keys |
| Extra infra to run | The bucket | The bucket | None (hosted) |
The classic S3 pattern pairs a bucket for storage with a native lock through use_lockfile; the S3 backend docs[10] mark the older dynamodb_table locking as deprecated, and GCS and HCP Terraform lock automatically.
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "network-automation/prod.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
State holds secrets in plaintext
Terraform writes every managed attribute into state exactly as the provider returns it, and that includes the sensitive ones: passwords, tokens, private keys, all in plaintext. Marking a variable sensitive only hides it from CLI output; it does nothing to the file, as the sensitive-data docs[11] state plainly. Two rules follow. First, never commit terraform.tfstate (or its .backup) to Git — a pushed state file leaks every secret to anyone who can read the repository. Second, treat the remote backend as a security control: encryption at rest, access control over who can read it, and audit logging. This confidentiality requirement is independent of locking, and the exam blurs the two on purpose: a backend can be perfectly locked and still expose every secret if its bucket is world-readable or its state is checked into a repository. Shared and locked answers a concurrency question; encrypted, access-controlled, and never in Git answers a secrecy question.
Reconciling and adopting state: refresh and import
Two operations change what is recorded in state without changing your infrastructure, and the exam leans on candidates confusing them with the shared-backend concurrency fix. Neither shares state and neither takes a lock.
Make state match reality: -refresh-only
Someone edits a resource in a console, or an object is deleted outside Terraform; now state disagrees with the real world. terraform apply -refresh-only reads every managed object back from the provider and updates state to match, letting you review the diff before it is written. It is the modern replacement for the deprecated terraform refresh: the refresh documentation[12] directs you to add -refresh-only to apply or plan instead. What refresh cannot do is invent a resource. If an object was created under another engineer's separate state file, it does not exist in yours, and reconciling an empty entry against the provider cannot conjure it into being — which is why refresh never solves a shared-state or concurrency problem.
Adopt an existing resource: import
When an object already exists but Terraform does not manage it — created by hand or another tool — terraform import <resource_address> <id> records its real ID against a resource address so Terraform adopts it instead of creating a duplicate, as the import documentation[13] describes:
terraform import iosxe_vlan.core <id> # adopt an existing VLAN into state
The <id> is the provider's documented import identifier for that resource. Newer Terraform also supports config-driven import: add an import block[14] to your configuration and the adoption happens on the next apply, which is reviewable and fits CI.
What neither one fixes
Both commands operate on a single state file. If the real problem is that two engineers hold separate state for the same infrastructure, the remedy is the shared, locked remote backend from the previous section — not -refresh-only, not import, and not swapping -var files. Keep the distinction crisp: refresh and import edit the contents of one state, while a remote backend changes whose state it is.
Drift detection and the lifecycle block
Drift is any difference between the real infrastructure and Terraform's recorded state, and terraform plan is what finds it. The classic cause on a network device is an out-of-band change: someone fixes something quickly at the CLI, the running config now carries a VLAN or ACL line Terraform never put there, and recorded state no longer matches reality.
plan closes the gap in two moves. First it refreshes recorded state against the live objects[4]; then it diffs that refreshed picture against your HCL and prints the actions to reconcile them. A drifted attribute shows up as the ~ update symbol from the plan-reading section, and running apply pulls the device back to the configured value. One point trips people up: plan changes nothing — refresh and diff are both read-only, and only apply mutates a device.
When the manual change was intentional
Re-converging is not always what you want; sometimes the out-of-band change is correct and should stay. Three directions are in play, and keeping them apart is the whole exam point. plan then apply pulls the device back to the HCL. terraform apply -refresh-only (from the refresh section) pulls state toward the device, accepting the new reality into state without touching your configuration. And to keep the change on the device without either overwriting it or rewriting state, you reach for the lifecycle block.
The lifecycle block: control how apply replaces and protects
A lifecycle block nested in a resource overrides apply's defaults, and each meta-argument[15] answers a different question.
resource "iosxe_vlan" "core" {
vlan_id = 10
name = "core"
lifecycle {
prevent_destroy = true
create_before_destroy = true
ignore_changes = [name]
}
}
| Meta-argument | What it controls | Reach for it when |
|---|---|---|
prevent_destroy | Rejects any plan that would destroy the resource | Loss of the resource is unacceptable |
create_before_destroy | Builds the replacement before destroying the old one | A replacement must not cause an outage |
ignore_changes | Ignores drift on the named attributes (or all) | An out-of-band change should be kept |
prevent_destroy = true makes Terraform reject any plan that would destroy the resource and error out instead. Do not misread it as a delete you can force through: it also blocks terraform destroy and any replacement, so to genuinely remove the resource you first delete the prevent_destroy line. create_before_destroy = true inverts the default destroy-then-create order — it builds the replacement, cuts over, then destroys the old object, closing the outage window and flipping the plan symbol from -/+ to +/-. ignore_changes is the tool for a manual change you want to keep: it tells plan to disregard drift on the attributes you list (or the keyword all), so an out-of-band edit to one of those fields stops showing up as a diff.
Scaling config: variables, modules, count vs for_each
The blocks so far were hard-coded for one VLAN. Real configurations are parameterized so one definition serves many values, and iterated so one block manages a whole set. That is the job of variables, modules, and the two iteration meta-arguments.
Variables, locals, outputs, and modules
Input variables[16] are the knobs a configuration exposes, locals[17] name an expression you reuse, and outputs[18] publish a value for a caller or another module. A module[19] is a folder of resources you call as a unit, passing inputs in and reading outputs back; the configuration you run in is the root module, and anything it calls is a child module. Packaging a device-onboarding pattern once and calling it per site is how a team keeps hundreds of devices described without copy-paste.
variable "vlans" {
type = map(string)
default = { "10" = "core", "20" = "voice", "30" = "guest" }
}
count versus for_each
Reach for for_each by default and keep count for genuinely interchangeable instances. Both stamp one resource across a collection, but they key the instances differently, and that difference is the whole exam point.
| Concern | count | for_each |
|---|---|---|
| Takes | A whole number N | A map or a set |
| Keyed by | Integer index 0..N-1 | Map key or set value |
| Instance address | iosxe_vlan.campus[0] | iosxe_vlan.campus["10"] |
| Remove a middle item | Later indexes shift; destroy/recreate | Only that instance changes |
for_each keys each instance by its map key or set value[20], exposed inside the block as each.key and each.value:
resource "iosxe_vlan" "campus" {
for_each = var.vlans
vlan_id = tonumber(each.key)
name = each.value
}
This makes iosxe_vlan.campus["10"], ["20"], and ["30"]. Because each instance is addressed by its key, removing "20" plans to destroy only that VLAN. count instead indexes each instance 0..N-1 through count.index[21], so the trap is positional: remove an element from the middle of a list and every later instance shifts down one index, so Terraform sees them as changed and destroys or recreates them. HashiCorp's own guidance is to use count for nearly identical instances and for_each when instances must have distinct values — which, for anything keyed by a name, a VLAN ID, or an interface, means for_each.
The Cisco provider family: RESTCONF, YANG, providers
Every Cisco Terraform provider does one job: it turns a block of HCL into a call against a Cisco platform's model-driven API, and turns that platform's data model back into Terraform state. Hold that one idea and the topic collapses into two questions — how you wire a provider up, and what its resources map to.
A Terraform provider is a plugin Terraform core loads to manage one kind of infrastructure; the CiscoDevNet[22] namespace publishes the ones for Cisco gear. The CiscoDevNet/iosxe[23] provider manages Catalyst and IOS-XE devices by driving their model-driven programmatic API — the device's YANG data model exposed over a wire protocol the provider speaks for you. It speaks two: NETCONF over SSH (the current default) and RESTCONF — the HTTP interface defined in RFC 8040[24] that presents the same YANG model as a URL tree you can GET, PATCH, or DELETE. RESTCONF is the transport this section illustrates; either way YANG is the schema, and the IOS-XE programmability stack[25] serves both on the box.
A resource argument is a YANG leaf, not a CLI line. When you declare an iosxe_vlan[26] with vlan_id = 10 and name = "USERS", the provider does not type vlan 10 into a terminal; it writes the id and name leaves of the native VLAN model over RESTCONF. Because it reads the same leaves back, Terraform detects drift at the attribute level, which a raw script or an imperative CLI push cannot do. The path from HCL to device runs through four layers, each translating the one above it: the resource block, Terraform core plus the provider plugin, the RESTCONF request on the wire, and the YANG datastore on the device.
Wiring a provider: required_providers, then provider
Two blocks stand between an empty directory and a managed device. required_providers pins which plugin and which version; the provider block supplies the connection.
terraform {
required_providers {
iosxe = {
source = "CiscoDevNet/iosxe"
version = "~> 0.18"
}
}
}
provider "iosxe" {
host = "198.51.100.1"
username = "admin"
password = var.device_password
protocol = "restconf"
insecure = true
}
The source is a registry address[27] in NAMESPACE/TYPE form, resolving to registry.terraform.io/CiscoDevNet/iosxe; the version is a constraint, so ~> 0.18 accepts any 0.18.x release. terraform init reads these and downloads the plugin, which is why init must succeed before any plan. The provider block's arguments[28] are defined by the provider itself: for IOS-XE, the host, username, password, a protocol, and insecure. The provider speaks both transports over the same model, and protocol picks which — it defaults to netconf (SSH), so set protocol = "restconf" when you want the RESTCONF path shown above. (host is the current, protocol-agnostic connection argument; the older RESTCONF-only url still works but is deprecated in favour of host.) Read insecure = true carefully — over RESTCONF it does not disable authentication and it does not drop to plain HTTP; it only skips TLS-certificate verification, the shortcut you take against a lab device's self-signed certificate and remove for production. Keep the password in a variable, never a literal in version control.
One workflow, device to controller
The payoff is that the controller goes behind the same Terraform as the device. Cisco publishes a family of providers[22], each running the identical HCL, state, and plan/apply loop; only the backing API and object model change underneath.
iosxe[23] drives RESTCONF on Catalyst / IOS-XE (iosxe_vlan,iosxe_interface_ethernet,iosxe_ospf).nxos[29] drives the NX-OS REST model on Nexus.aci[30] drives the APIC REST API for an ACI fabric (aci_tenant).sdwan[31] drives Catalyst SD-WAN Manager.catalystcenter[32] drives Catalyst Center.
The split that matters is device versus controller: iosxe and nxos talk to one box each, so you scale them across an inventory, while aci, sdwan, and catalystcenter talk to a single controller that already fronts many devices. The overview's comparison table and decision tree map the whole family, and the getting-started path[33] is the same for all of them. When a stem asks to manage IOS-XE config declaratively with drift detection, the answer is the iosxe provider driving that model-driven API: Ansible ios_config is imperative and keeps no state, a hand-written RESTCONF script has no plan and no drift detection, and SNMP is read-mostly. Terraform state is the drift-detection tell.
Cisco Terraform provider family
| Attribute | iosxe | nxos | aci | sdwan | catalystcenter |
|---|---|---|---|---|---|
| Backing API | IOS-XE NETCONF/RESTCONF | NX-OS REST | APIC REST | SD-WAN Manager REST | Catalyst Center REST |
| Manages | One device | One device | Fabric (APIC) | Overlay (Manager) | Controller |
| Example resource | iosxe_vlan | nxos_ospf | aci_tenant | sdwan_cisco_system_feature_template | catalystcenter_template |
| Typical target | Catalyst / IOS-XE | Nexus | ACI fabric | Catalyst SD-WAN | Catalyst Center |
Decision tree
Sharp facts the exam loves — give these one last read before exam day.
Cheat sheet
Sharp facts the exam loves — scan these before test day.
- State maps config to real resources and is authoritative
Terraform's
terraform.tfstateis the source of truth mapping HCL resources to real objects; a state file that is local to one runner is invisible to others, so a second engineer's run believes the resources do not exist and may recreate or destroy them.12 questions test this
- After a refactor, an engineer renames a managed resource from aws_instance.web to aws_instance.frontend in the HCL only, using the shared remote backend. terraform plan now proposes to destroy the exi
- An engineer manages subnets and firewall objects on cloud-hosted appliances with Terraform, keeping terraform.tfstate only on a laptop with no backups and no remote backend. A disk-cleanup script dele
- A team is moving from local state to a design where all engineers and the GitLab CI runner share one state, and the top requirement is that two terraform apply operations starting seconds apart must n
- Two engineers on the same team provision an identical set of firewall rules to a cloud-managed appliance from the same HCL, but each keeps their own terraform.tfstate on their laptop instead of a shar
- A virtual machine that Terraform manages from the shared remote backend had its instance size changed directly in the cloud console. The engineer wants Terraform's state to record the new size and acc
- Two separate Terraform root modules, each with its own remote backend and state, were independently written to manage the same production load balancer. Each team runs terraform apply on its own sched
- An engineer removes the aws_instance.web resource block from an otherwise unchanged Terraform configuration and runs terraform apply against a shared remote backend. The plan proposes to destroy the r
- An engineer wants Terraform to stop managing a legacy load balancer, removing it from both the configuration and state, while leaving the live load balancer running for a separate team. Simply deletin
- A DNS zone was created manually months before Terraform was adopted and must now be managed from the shared remote backend. An engineer intends to run terraform import for the zone. To predict the out
- Engineer A created several VPC subnets using their own local terraform.tfstate. Engineer B, working from a fresh clone with an empty state, wants Terraform to recognize those existing subnets and runs
- A team migrated Terraform state from local files to a remote backend shared by all engineers and the CI runner. Occasionally two terraform apply operations begin within seconds of each other against t
- A network automation team keeps Terraform state in an S3 bucket shared by every engineer and the GitLab CI runner. They need two near-simultaneous terraform apply runs to be serialized so the second w
- Remote backends with state locking serialize applies
Moving state to a remote backend (Amazon S3 with native
use_lockfilelocking, Google Cloud Storage, or HCP Terraform) shares one authoritative state and enables state locking so only oneterraform applycan mutate state at a time, preventing concurrent destructive operations.Trap
terraform refreshor-varfiles are offered as the fix, but only a shared, locked remote backend addresses the concurrency root cause.6 questions test this
- A team is moving from local state to a design where all engineers and the GitLab CI runner share one state, and the top requirement is that two terraform apply operations starting seconds apart must n
- Two engineers on the same team provision an identical set of firewall rules to a cloud-managed appliance from the same HCL, but each keeps their own terraform.tfstate on their laptop instead of a shar
- An engineer configures a remote backend with state locking for the team's Terraform workflow and assumes it will also stop other teams from editing those resources directly in the cloud console. Durin
- Two separate Terraform root modules, each with its own remote backend and state, were independently written to manage the same production load balancer. Each team runs terraform apply on its own sched
- A team migrated Terraform state from local files to a remote backend shared by all engineers and the CI runner. Occasionally two terraform apply operations begin within seconds of each other against t
- A network automation team keeps Terraform state in an S3 bucket shared by every engineer and the GitLab CI runner. They need two near-simultaneous terraform apply runs to be serialized so the second w
- Refresh reconciles state but cannot invent missing resources
terraform apply -refresh-only(formerlyterraform refresh) updates state to match the real world, but it cannot synthesize resources created under a different engineer's separate state file, so it does not solve shared-state or concurrency problems.6 questions test this
- A virtual machine that Terraform manages from the shared remote backend had its instance size changed directly in the cloud console. The engineer wants Terraform's state to record the new size and acc
- A CI runner's remote state was accidentally restored to a versioned backup from last month, so several subnets created since then are no longer recorded in state even though they still exist in the cl
- Two separate Terraform root modules, each with its own remote backend and state, were independently written to manage the same production load balancer. Each team runs terraform apply on its own sched
- Engineer A created several VPC subnets using their own local terraform.tfstate. Engineer B, working from a fresh clone with an empty state, wants Terraform to recognize those existing subnets and runs
- A firewall object that Terraform manages from a shared remote backend was deleted directly in the cloud console by another team. The engineer wants Terraform's state to accurately reflect that the obj
- A subnet was created manually in the cloud console before Terraform was adopted. The matching HCL resource now exists in the configuration, but terraform plan proposes to create a new subnet, risking
- terraform import adopts pre-existing resources into state
terraform import <resource_address> <id>brings an already-existing, unmanaged object under Terraform management by recording it in state; without import, Terraform would try to create a duplicate of a resource it does not know about.8 questions test this
- After a refactor, an engineer renames a managed resource from aws_instance.web to aws_instance.frontend in the HCL only, using the shared remote backend. terraform plan now proposes to destroy the exi
- A CI runner's remote state was accidentally restored to a versioned backup from last month, so several subnets created since then are no longer recorded in state even though they still exist in the cl
- An engineer wants Terraform to stop managing a legacy load balancer, removing it from both the configuration and state, while leaving the live load balancer running for a separate team. Simply deletin
- A DNS zone was created manually months before Terraform was adopted and must now be managed from the shared remote backend. An engineer intends to run terraform import for the zone. To predict the out
- Engineer A created several VPC subnets using their own local terraform.tfstate. Engineer B, working from a fresh clone with an empty state, wants Terraform to recognize those existing subnets and runs
- An engineer runs terraform import aws_security_group.legacy sg-0abc123 to bring an existing, previously unmanaged security group under Terraform control. The command succeeds and the group now appears
- A firewall object that Terraform manages from a shared remote backend was deleted directly in the cloud console by another team. The engineer wants Terraform's state to accurately reflect that the obj
- A subnet was created manually in the cloud console before Terraform was adopted. The matching HCL resource now exists in the configuration, but terraform plan proposes to create a new subnet, risking
- Terraform state stores secrets in plaintext
terraform.tfstate persists every managed resource attribute — including passwords, tokens, and keys — in plaintext, so the state file must live in an encrypted, access-controlled remote backend and never be committed to Git; this secrecy concern is separate from state locking.
Trap Committing terraform.tfstate to the repo or assuming state locking also protects its confidentiality.
- terraform init installs providers and initializes the backend
terraform initdownloads the providers declared inrequired_providersand initializes the configured backend; it must run beforeplan/applyand again whenever providers, modules, or the backend change.- plan -out guarantees apply executes exactly that plan
terraform plancomputes an execution plan (the diff) andterraform applyexecutes it; saving the plan with-out=plan.tfplanand applying that file guarantees apply performs exactly the reviewed actions with no re-computation.5 questions test this
- A CI pipeline stage runs terraform plan -out=fabric.tfplan against a Cisco ACI configuration and archives the file for a later approval stage. Before apply runs, another engineer applies an out-of-ban
- An engineer runs terraform plan (without -out) against a Cisco Meraki configuration, reviews the diff, and returns the next day to run terraform apply in interactive mode. The engineer wants assurance
- A pipeline runs terraform plan -out=meraki.tfplan against a Cisco Meraki configuration, drawing network settings from terraform.tfvars, and archives the file for a later approval stage. Between the pl
- A network team enforces a change-control gate: the exact diff a reviewer approves must be the exact set of changes Terraform executes against the Cisco NX-OS fabric, with no possibility of recomputati
- An engineer runs terraform plan -out=sdwan.tfplan against a Cisco SD-WAN vManage configuration and hands the file to a colleague. The colleague, expecting the usual interactive 'Do you want to perform
- Plan action symbols: + create, - destroy, ~ update, -/+ replace
In plan output
+creates,-destroys,~updates in place, and-/+replaces (destroy then create) a resource; a-/+on a networking resource signals a disruptive recreation, often driven by a change to an immutable attribute.5 questions test this
- While reviewing terraform plan output for a Cisco NX-OS fabric, an engineer sees one VLAN interface resource annotated with the -/+ symbol and the note '# forces replacement' beside an immutable attri
- An engineer reviews a terraform plan for a Cisco IOS-XE access list managed through a RESTCONF provider. One resource line begins with the ~ symbol where only an ACE remark text changes, while a separ
- An engineer runs terraform plan against a configuration managing Cisco Meraki networks and reads the summary line 'Plan: 3 to add, 2 to change, 1 to destroy.' In the detailed output, one resource line
- While reviewing terraform plan output for a Cisco IOS-XE loopback interface managed through a RESTCONF provider, an engineer sees the resource annotated with the -/+ symbol. The engineer needs to pred
- While reviewing a terraform plan for a Cisco ACI tenant migration, an engineer sees a bridge domain resource annotated with the -/+ symbol, and the plan notes the change is forced by a modification to
- destroy removes managed resources; -target scopes actions
terraform destroyremoves all resources tracked in state, while-target=<address>scopes a plan/apply/destroy to a single resource or module;-targetis a surgical escape hatch, not a routine workflow.- required_providers pins provider source and version
The
terraform { required_providers { iosxe = { source = "CiscoDevNet/iosxe", version = "..." } } }block declares each plugin's registry source and a version constraint; a matchingprovider "iosxe"block then setsurl,username,password, andinsecure.5 questions test this
- An engineer initializes a Terraform project to manage Catalyst 9300 switches, but 'terraform init' fails because the plugin cannot be located in the registry. Which declaration correctly pins the Cisc
- An engineer is reviewing a Terraform configuration and must explain the division of responsibility between a required_providers entry that sets source = 'CiscoDevNet/iosxe' and version = '~> 0.5' and
- An organization standardizes on one Terraform HCL/state/plan workflow to automate Catalyst 9000 IOS-XE switches, an ACI fabric, and an SD-WAN overlay managed through vManage. Which set of Cisco-publis
- A team has one Terraform code base that manages Catalyst 9000 IOS-XE switches directly and now must extend the same HCL/state/plan workflow to configure a data-center fabric built on Nexus 9000 switch
- An engineer copies HCL from an internal wiki whose terraform { required_providers { ... } } block declares the iosxe provider with a version constraint but sets source = 'hashicorp/iosxe'. terraform i
- The IOS-XE provider manages devices over RESTCONF
The
CiscoDevNet/iosxeTerraform provider configures Catalyst/IOS-XE devices by driving their model-driven API — NETCONF over SSH by default, or RESTCONF when you setprotocol = "restconf"— exposing resources such asiosxe_vlan,iosxe_interface_ethernet, andiosxe_ospfwhose attributes map to native YANG leaves.7 questions test this
- After deploying VLAN and OSPF configuration to a Catalyst 9500 with the CiscoDevNet/iosxe provider, a technician makes an out-of-band change via the switch CLI, modifying the description on an interfa
- An engineer initializes a Terraform project to manage Catalyst 9300 switches, but 'terraform init' fails because the plugin cannot be located in the registry. Which declaration correctly pins the Cisc
- An engineer must automate an enterprise campus where devices are onboarded and configured centrally through a Catalyst Center controller rather than device by device, and wants to keep the same Terraf
- Using the CiscoDevNet/iosxe provider, an engineer must declaratively create a set of access VLANs on Catalyst switches and have Terraform reconcile the device to exactly the declared VLANs on each app
- A team plans to manage IOS-XE Catalyst switches with the CiscoDevNet/iosxe Terraform provider and finds that 'terraform apply' fails to reach the devices even though credentials are correct. Which sta
- A campus automation team manages Catalyst 9300 switches with the CiscoDevNet/iosxe Terraform provider. Their HCL declares iosxe_vlan, iosxe_interface_ethernet, and iosxe_ospf resources, and a provider
- An engineer writes HCL that declares an 'iosxe_vlan' resource and a matching 'provider iosxe' block with a URL and credentials for a Catalyst 9300 switch, but every 'terraform apply' fails to communic
- Cisco publishes providers for IOS-XE, NX-OS, ACI, SD-WAN, Catalyst Center
Cisco maintains Terraform providers including
iosxe,nxos,aci,sdwan, andcatalystcenter, so the same HCL/state/plan workflow spans device-level and controller-level automation across product lines.5 questions test this
- An engineer must automate an enterprise campus where devices are onboarded and configured centrally through a Catalyst Center controller rather than device by device, and wants to keep the same Terraf
- Using the CiscoDevNet/iosxe provider, an engineer must declaratively create a set of access VLANs on Catalyst switches and have Terraform reconcile the device to exactly the declared VLANs on each app
- An organization standardizes on one Terraform HCL/state/plan workflow to automate Catalyst 9000 IOS-XE switches, an ACI fabric, and an SD-WAN overlay managed through vManage. Which set of Cisco-publis
- A team has one Terraform code base that manages Catalyst 9000 IOS-XE switches directly and now must extend the same HCL/state/plan workflow to configure a data-center fabric built on Nexus 9000 switch
- An engineer copies HCL from an internal wiki whose terraform { required_providers { ... } } block declares the iosxe provider with a version constraint but sets source = 'hashicorp/iosxe'. terraform i
- Provider resource attributes mirror the underlying YANG model
Each Cisco provider resource's arguments correspond to leaves in the device's YANG model (for example an
iosxe_vlanresource'svlan_idandname), so Terraform effectively becomes a declarative front end over model-driven configuration.- HCL declares desired end state, not procedural steps
HCL is declarative: you describe the desired end state of resources and Terraform's core computes the create/update/destroy actions to reach it, in contrast to an imperative script where you code each step and ordering yourself.
4 questions test this
- An engineer new to Terraform ports a Bash script that runs a fixed sequence of iosxe CLI commands to add three VLANs into an HCL configuration. In review, a senior engineer notes the HCL file lists th
- An engineer new to Terraform asks how to add procedural retry logic and an explicit rollback sequence into an HCL configuration that manages iosxe_vlan and iosxe_interface resources, similar to a shel
- A team manages 40 access-layer switch VLANs with Terraform and a remote backend. Overnight, an operator adds VLAN 999 directly on one switch through the CLI and renames VLAN 20 out of band. The next m
- A network engineer accustomed to shell scripting writes a Terraform configuration for a Cisco IOS-XE device that declares an iosxe_interface resource for Loopback0 and an iosxe_ospf resource whose rou
- plan detects drift between state and real infrastructure
Configuration drift occurs when real infrastructure diverges from state (e.g., a manual out-of-band CLI change);
terraform planrefreshes and surfaces that drift as a diff, and a subsequentapplyre-converges the device to the HCL-defined state.4 questions test this
- A scheduled GitLab CE job runs terraform plan every night against a Cisco IOS-XE fabric managed with a remote backend and emails the output to the on-call engineer. During the day an operator used the
- A team manages 40 access-layer switch VLANs with Terraform and a remote backend. Overnight, an operator adds VLAN 999 directly on one switch through the CLI and renames VLAN 20 out of band. The next m
- A GitLab CE scheduled job must run hourly against a Cisco IOS-XE fabric managed by Terraform with a remote backend and automatically open a ticket only when real infrastructure has drifted from state.
- An operator made several out-of-band CLI changes on Cisco IOS-XE devices that Terraform manages. The team has confirmed the running infrastructure is now correct but has not yet updated the HCL. Befor
- Variables, modules, and for_each parameterize and scale config
Input variables,
locals, andoutputsparameterize a configuration; reusablemodules package it; andcount/for_eachiterate a resource across a set (e.g., oneiosxe_vlanper entry in a VLAN map) for DRY, scalable definitions.5 questions test this
- An engineer must provision one iosxe_vlan resource per entry in a variable named vlans, defined as a map keyed by VLAN name with vlan_id values. The requirement is that later removing a single key fro
- A team has a working site module that configures management VLANs, an OSPF process, and uplink interfaces for one branch. They must now deploy the identical module to 30 branches, each with different
- An engineer splits a Terraform configuration into two child modules: a vlans module that creates iosxe_vlan resources and computes their allocated IDs, and an svi module that must create a switched vi
- A team wants a single reusable Terraform configuration that different sites can call with their own VLAN data, and they must guarantee that any VLAN ID outside the range 2-1001 is rejected before Terr
- An engineer must create one iosxe_vlan resource for each entry in a variable named vlans, defined as a map whose keys are VLAN names and whose values contain the VLAN ID and description. Inside the re
- lifecycle meta-arguments control replacement behavior
The
lifecycleblock tunes resource replacement:prevent_destroyblocks accidental deletion,create_before_destroyavoids an outage during replacement, andignore_changestells Terraform to disregard drift on specific attributes.
Also tested in
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
- TA-004 HashiCorp Certified: Terraform Associate
References
- https://developer.hashicorp.com/terraform/language/syntax/configuration
- https://developer.hashicorp.com/terraform/intro/core-workflow
- https://developer.hashicorp.com/terraform/cli/commands/init
- https://developer.hashicorp.com/terraform/cli/commands/plan
- https://developer.hashicorp.com/terraform/tutorials/cli/plan
- https://developer.hashicorp.com/terraform/language/resources/behavior
- https://developer.hashicorp.com/terraform/cli/state/resource-addressing
- https://developer.hashicorp.com/terraform/language/state
- https://developer.hashicorp.com/terraform/language/state/locking
- https://developer.hashicorp.com/terraform/language/backend/s3
- https://developer.hashicorp.com/terraform/language/state/sensitive-data
- https://developer.hashicorp.com/terraform/cli/commands/refresh
- https://developer.hashicorp.com/terraform/cli/commands/import
- https://developer.hashicorp.com/terraform/language/import
- https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle
- https://developer.hashicorp.com/terraform/language/values/variables
- https://developer.hashicorp.com/terraform/language/values/locals
- https://developer.hashicorp.com/terraform/language/values/outputs
- https://developer.hashicorp.com/terraform/language/modules
- https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
- https://developer.hashicorp.com/terraform/language/meta-arguments/count
- https://developer.cisco.com/iac/
- https://registry.terraform.io/providers/CiscoDevNet/iosxe/latest/docs
- https://www.rfc-editor.org/rfc/rfc8040
- https://developer.cisco.com/iosxe/
- https://registry.terraform.io/providers/CiscoDevNet/iosxe/latest/docs/resources/vlan
- https://developer.hashicorp.com/terraform/language/providers/requirements
- https://developer.hashicorp.com/terraform/language/providers/configuration
- https://registry.terraform.io/providers/CiscoDevNet/nxos/latest/docs
- https://registry.terraform.io/providers/CiscoDevNet/aci/latest/docs
- https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs
- https://registry.terraform.io/providers/CiscoDevNet/catalystcenter/latest/docs
- https://developer.cisco.com/automation-terraform/