Domain 1 of 4 · Chapter 2 of 6

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.

Configuration desired state State known reality terraform init terraform plan terraform apply Managed device terraform destroy diff applies desired actual removes all
The Terraform loop: init prepares providers and the backend, plan diffs configuration against state, apply provisions the device, and destroy removes all state.

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.

API can update in place? ~ update in place create_before_destroy set? -/+ destroy, then create +/- create, then destroy Yes No No Yes
A changed argument updates in place when the API allows it; otherwise it replaces the resource, as +/- when create_before_destroy is set and -/+ when it is not.

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.

Shared cloud one real set of objects Engineer A local terraform.tfstate Engineer B local terraform.tfstate apply apply No shared state: each map differs Terraform may recreate or destroy live objects
Separate local state for one shared target: each engineer's map differs, so Terraform may recreate or destroy live resources.

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.

Config convergedManual CLI changeon the deviceRecorded state now staleterraform planrefresh, diff shows driftOverwriteAcceptterraform applyre-converge to HCLignore_changesaccept the change
Drift lifecycle: an out-of-band change makes recorded state stale; plan surfaces it, and you either apply to re-converge or ignore_changes to accept it.

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.

resource "iosxe_vlan" desired VLAN state in HCL terraform apply Terraform core + provider plugin CiscoDevNet/iosxe renders model-driven request RESTCONF over HTTPS HTTP PATCH, JSON body writes YANG leaves IOS-XE running datastore native YANG data model
HCL to device in four layers: the iosxe_vlan resource, Terraform plus the CiscoDevNet plugin, the RESTCONF PATCH, and the IOS-XE YANG datastore.

Cisco Terraform provider family

Attributeiosxenxosacisdwancatalystcenter
Backing APIIOS-XE NETCONF/RESTCONFNX-OS RESTAPIC RESTSD-WAN Manager RESTCatalyst Center REST
ManagesOne deviceOne deviceFabric (APIC)Overlay (Manager)Controller
Example resourceiosxe_vlannxos_ospfaci_tenantsdwan_cisco_system_feature_templatecatalystcenter_template
Typical targetCatalyst / IOS-XENexusACI fabricCatalyst SD-WANCatalyst Center

Decision tree

Controller or single device? single device controller IOS-XE or Nexus? Which controller? IOS-XE Nexus ACI SD-WAN Catalyst Center iosxe RESTCONF nxos NX-OS REST aci APIC sdwan SD-WAN Manager catalystcenter Catalyst Center

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.tfstate is 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
Remote backends with state locking serialize applies

Moving state to a remote backend (Amazon S3 with native use_lockfile locking, Google Cloud Storage, or HCP Terraform) shares one authoritative state and enables state locking so only one terraform apply can mutate state at a time, preventing concurrent destructive operations.

Trap terraform refresh or -var files are offered as the fix, but only a shared, locked remote backend addresses the concurrency root cause.

6 questions test this
Refresh reconciles state but cannot invent missing resources

terraform apply -refresh-only (formerly terraform 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
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
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 init downloads the providers declared in required_providers and initializes the configured backend; it must run before plan/apply and again whenever providers, modules, or the backend change.

1 question tests this
plan -out guarantees apply executes exactly that plan

terraform plan computes an execution plan (the diff) and terraform apply executes it; saving the plan with -out=plan.tfplan and applying that file guarantees apply performs exactly the reviewed actions with no re-computation.

5 questions test this
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
destroy removes managed resources; -target scopes actions

terraform destroy removes all resources tracked in state, while -target=<address> scopes a plan/apply/destroy to a single resource or module; -target is 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 matching provider "iosxe" block then sets url, username, password, and insecure.

5 questions test this
The IOS-XE provider manages devices over RESTCONF

The CiscoDevNet/iosxe Terraform provider configures Catalyst/IOS-XE devices by driving their model-driven API — NETCONF over SSH by default, or RESTCONF when you set protocol = "restconf" — exposing resources such as iosxe_vlan, iosxe_interface_ethernet, and iosxe_ospf whose attributes map to native YANG leaves.

7 questions test this
Cisco publishes providers for IOS-XE, NX-OS, ACI, SD-WAN, Catalyst Center

Cisco maintains Terraform providers including iosxe, nxos, aci, sdwan, and catalystcenter, so the same HCL/state/plan workflow spans device-level and controller-level automation across product lines.

5 questions test this
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_vlan resource's vlan_id and name), 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
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 plan refreshes and surfaces that drift as a diff, and a subsequent apply re-converges the device to the HCL-defined state.

4 questions test this
Variables, modules, and for_each parameterize and scale config

Input variables, locals, and outputs parameterize a configuration; reusable modules package it; and count/for_each iterate a resource across a set (e.g., one iosxe_vlan per entry in a VLAN map) for DRY, scalable definitions.

5 questions test this
lifecycle meta-arguments control replacement behavior

The lifecycle block tunes resource replacement: prevent_destroy blocks accidental deletion, create_before_destroy avoids an outage during replacement, and ignore_changes tells Terraform to disregard drift on specific attributes.

Also tested in

References

  1. https://developer.hashicorp.com/terraform/language/syntax/configuration
  2. https://developer.hashicorp.com/terraform/intro/core-workflow
  3. https://developer.hashicorp.com/terraform/cli/commands/init
  4. https://developer.hashicorp.com/terraform/cli/commands/plan
  5. https://developer.hashicorp.com/terraform/tutorials/cli/plan
  6. https://developer.hashicorp.com/terraform/language/resources/behavior
  7. https://developer.hashicorp.com/terraform/cli/state/resource-addressing
  8. https://developer.hashicorp.com/terraform/language/state
  9. https://developer.hashicorp.com/terraform/language/state/locking
  10. https://developer.hashicorp.com/terraform/language/backend/s3
  11. https://developer.hashicorp.com/terraform/language/state/sensitive-data
  12. https://developer.hashicorp.com/terraform/cli/commands/refresh
  13. https://developer.hashicorp.com/terraform/cli/commands/import
  14. https://developer.hashicorp.com/terraform/language/import
  15. https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle
  16. https://developer.hashicorp.com/terraform/language/values/variables
  17. https://developer.hashicorp.com/terraform/language/values/locals
  18. https://developer.hashicorp.com/terraform/language/values/outputs
  19. https://developer.hashicorp.com/terraform/language/modules
  20. https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
  21. https://developer.hashicorp.com/terraform/language/meta-arguments/count
  22. https://developer.cisco.com/iac/
  23. https://registry.terraform.io/providers/CiscoDevNet/iosxe/latest/docs
  24. https://www.rfc-editor.org/rfc/rfc8040
  25. https://developer.cisco.com/iosxe/
  26. https://registry.terraform.io/providers/CiscoDevNet/iosxe/latest/docs/resources/vlan
  27. https://developer.hashicorp.com/terraform/language/providers/requirements
  28. https://developer.hashicorp.com/terraform/language/providers/configuration
  29. https://registry.terraform.io/providers/CiscoDevNet/nxos/latest/docs
  30. https://registry.terraform.io/providers/CiscoDevNet/aci/latest/docs
  31. https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs
  32. https://registry.terraform.io/providers/CiscoDevNet/catalystcenter/latest/docs
  33. https://developer.cisco.com/automation-terraform/