Drift Detection and Import
The idea in one sentence
Section titled “The idea in one sentence”Configuration drift happens when reality changes outside Terraform, and import is how you bring a real resource that already exists into Terraform’s state without recreating it.
Detecting drift
Section titled “Detecting drift”State is only a recorded snapshot of the real world at the moment of the last apply. Nothing stops someone from opening the Azure Portal and changing a resource directly — bumping a virtual machine’s tag, resizing a storage account’s tier, or opening a network security group rule by hand. Terraform has no way to know that happened until it looks again.
resource "azurerm_linux_virtual_machine" "web" { name = "web-vm" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location size = "Standard_B2s" admin_username = "azureuser"
network_interface_ids = [ azurerm_network_interface.web.id, ]
tags = { environment = "production" }}Suppose a teammate edits that VM’s environment tag in the Azure Portal to "staging", outside of any Terraform run. The next time anyone runs terraform plan, Terraform refreshes its view of the real resource, notices the tag no longer matches the configuration, and shows a diff — it proposes changing the tag back to "production", because as far as Terraform is concerned the configuration is the source of truth and the portal edit is drift to be corrected.
terraform plan
# ~ resource "azurerm_linux_virtual_machine" "web" {# ~ tags = {# ~ "environment" = "staging" -> "production"# }# }That is the normal, expected way plan surfaces drift — as a proposed change that will overwrite reality with the configuration the next time you apply. Sometimes you want the opposite: you want Terraform to just update its own records to match reality, without touching any actual infrastructure. That is what -refresh-only is for:
terraform apply -refresh-onlyThis mode refreshes state from the real Azure resources and shows you what changed, then asks you to confirm writing those changes into terraform.tfstate — but it never modifies Azure itself. It is the right tool when the drift was intentional and you have decided the portal change should win, or when you need to update your mental model of what state currently holds before deciding whether to fix the configuration or accept reality.
Bringing existing resources under management with import
Section titled “Bringing existing resources under management with import”Drift assumes Terraform was already managing the resource. A different, related problem is a resource that exists in Azure but was never created by Terraform at all — maybe it predates your Terraform adoption, or someone clicked it into existence in the portal during an outage. terraform import is the CLI command that associates that real resource with a resource address in your configuration:
terraform import azurerm_linux_virtual_machine.web /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-resources/providers/Microsoft.Compute/virtualMachines/web-vmRunning that command only writes a state entry — it maps azurerm_linux_virtual_machine.web to the real resource ID and pulls in all of that VM’s current attributes. It does not write the matching resource "azurerm_linux_virtual_machine" "web" { ... } block for you. You still have to hand-write HCL that matches the real resource’s attributes closely enough that the next plan comes back clean. If your configuration disagrees with reality after importing, plan will show the same kind of diff drift produces, and you will need to adjust your HCL — not the real resource — until they line up.
The declarative import block
Section titled “The declarative import block”Terraform 1.5 introduced a second way to import, written directly into your .tf files as a first-class block instead of a one-off CLI invocation:
import { to = azurerm_linux_virtual_machine.web id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-resources/providers/Microsoft.Compute/virtualMachines/web-vm"}This does the same underlying job as terraform import, but with an important difference in workflow. The CLI command runs immediately and mutates state the moment you invoke it — there is no preview, and nothing to review before it happens. The import block, by contrast, is just configuration: it shows up as a planned action the next time you run terraform plan, alongside any other changes, before anything actually happens.
terraform plan
# Terraform will perform the following actions:## # azurerm_linux_virtual_machine.web will be imported# resource "azurerm_linux_virtual_machine" "web" {# id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-resources/providers/Microsoft.Compute/virtualMachines/web-vm"# size = "Standard_B2s"# ...# }That makes the import block the preferred approach wherever it is available: it is plannable, reviewable in a pull request like any other configuration change, and repeatable across environments, rather than being an immediate side effect that someone has to remember to run by hand on every machine that needs it. You still need to write the matching resource block yourself either way — the block only tells Terraform which real object to associate with which address.
Reorganizing state without touching real infrastructure
Section titled “Reorganizing state without touching real infrastructure”Two more commands round out the state-editing toolkit, both of which change only Terraform’s bookkeeping and never touch Azure:
# Move a resource to a new address in state, e.g. after refactoring into a moduleterraform state mv azurerm_linux_virtual_machine.web module.compute.azurerm_linux_virtual_machine.web
# Stop tracking a resource in state, without destroying the real objectterraform state rm azurerm_linux_virtual_machine.webterraform state mv is what you reach for when you restructure your configuration — say, moving a resource into a module — and do not want Terraform to see that as “destroy the old one, create a new one.” It updates the address in state to match the new location in your HCL, and the real Azure resource is never touched. terraform state rm is the opposite direction of import: it deletes a resource’s entry from state so Terraform forgets about it entirely, but the real object in Azure keeps running exactly as it was. That is useful when you want to hand a resource off to a different Terraform configuration, or deliberately stop managing something without deleting it.
flowchart LR portal["Manual change in Azure Portal"] --> drift["Resource now disagrees with config"] drift -->|terraform plan| diff["Diff shown: drift detected"] existing["Existing untracked Azure resource"] -->|terraform import or import block| state["Resource added to state"] state -->|next plan| clean["plan shows no diff, if config matches"]