Skip to content

Documentation / Infrastructure runs

Drift detection and health assessments

Plan-only runs on a timer that find drift and failing check blocks: turning them on, how often, what the dashboard shows, webhook alerts, and how they are billed.

A health assessment is a plan-only run that we start on a timer. It compares a workspace's state with your real infrastructure and evaluates its check blocks. You find out about drift, meaning something changed outside Terraform, before your next apply does.

There is no feature charge. Each assessment is a plan-only run, billed as ordinary run-minutes at your Terraform rate, with at least one minute per assessment.

Health assessments are switched on per workspace. An owner or an admin opens the workspace from Terraform in the dashboard, goes to its Settings tab and opens Health, ticks Enable health assessments and presses Save health assessments. Untick it and save to turn assessments off again. No further assessments start once they are off.

The first assessment starts soon after you turn them on. After that, the workspace is assessed on your organisation's schedule.

Over the Terraform API, the workspace attribute is assessments-enabled. With the tfe provider, it is assessments_enabled on tfe_workspace, and the value you set is the value that is read back:

hcl
resource "tfe_workspace" "prod" {
  name                = "prod"
  organization        = "my-org"
  assessments_enabled = true
}

Under Settings → Terraform settings, the Terraform drift checks section sets how often every workspace with assessments on is checked. The choices are every 15 minutes, 30 minutes, hour, 3 hours, 6 hours, 12 hours or 24 hours. 24 hours is the default. An owner or an admin can change it.

A workspace can choose its own interval from the same list. On the workspace's Settings tab, How often to check for drift offers Your organisation's schedule or any of the seven intervals, and the choice is saved with Save health assessments. A workspace's own interval wins over the organisation's. Choose Your organisation's schedule to follow the organisation again.

Both places price each choice at your run-minute rate before you pick it. A shorter interval finds drift sooner and costs more. An assessment never delays a run of yours: it waits while you are running, as described below.

Over the Terraform API, the workspace attribute is assessment-interval, with the same spellings as the organisation's: 15m, 30m, 1h, 3h, 6h, 12h or 24h. It reads null while the workspace follows its organisation, and you send "" to return to it. health.interval says how often the workspace is really assessed, and health.interval-source says whether that is the workspace or the organization choice. Any other value is refused with a 422.

An assessment needs something to plan against. It uses the configuration your infrastructure was last applied from: the newest of a run that applied, a run that found nothing to change, or a configuration you uploaded after applying it yourself. It never uses a speculative plan from a pull request. A run that failed or was discarded before it began applying changed nothing, so it is passed over. A workspace connected to version control that has never run is not assessed until it has.

If the newest apply failed part way through, your state may hold some of that configuration and not the rest. Assessments then pause, and the workspace's Settings tab names the run. They resume after a successful apply, or after you upload the configuration you applied.

A CLI-driven workspace that applies on your own machine and only plans here tells us what it applied by uploading it. After each apply, upload the configuration you applied as a configuration version with speculative: false and auto-queue-runs: false. A terraform plan or tofu plan from the CLI uploads a speculative configuration version, so it is never assessed. Until you upload a non-speculative configuration version, the workspace is not assessed and its Settings tab says so. Assessments then compare your infrastructure with the code you applied, even if a remote run on the workspace failed earlier.

These workspaces are not assessed, and the workspace's Settings tab says why:

  • a workspace using local execution, because its runs happen on your own machine;

  • a workspace pinned to a version older than 0.15.4, whose plans cannot report drift.

An assessment waits while the workspace is locked or one of your own runs is queued or in progress, and tries again later. An assessment never takes the workspace lock, so it never blocks you.

Drift means a change made outside Terraform to something your configuration sets. HCP Terraform draws the same line: drift detection "only reports on changes to the resource attributes defined in your configuration". An assessment is a refresh-only plan. It is never combined with a normal plan, so code you have changed but not applied yet never reads as drift.

For each resource that changed outside Terraform, the assessment counts only the attributes your configuration sets:

the attribute iscounted as drift
set in your configurationyes
set in your configuration, but listed in lifecycle { ignore_changes = [...] }no
any attribute of a resource with ignore_changes = allno
not set in your configurationno

So these are not drift:

  • A value only the provider sets, such as a Cloud Run job's execution count going up every time the job runs, or an etag moving.

  • An attribute you never wrote, even one your next plan would reset to the provider's default. Google's google_cloud_run_v2_job has max_retries, which defaults to 3. If your code does not set it and someone changes it to 5 in the console, the assessment does not report it.

  • An attribute you ignore. gcloud stamps client and client_version on every Cloud Run service and job it deploys. Many configurations ignore them, and the assessment does too.

A resource deleted or replaced outside Terraform is always drift, whatever it ignores, because your next plan would recreate it.

The assessment reads ignore_changes and dynamic blocks from the configuration the assessment ran with. An attribute set inside a dynamic block counts the same as one written out in full.

When the assessment cannot read your configuration, it counts more as drift, never less. This happens for a module from a registry or git, a module with an override.tf file, a .tf.json file with dynamic blocks, or a file that does not parse. For those resources, ignore_changes is not applied, and an attribute you did not set counts unless only the provider sets it. A value the provider treats as equal to the old one, such as the same JSON policy with its keys in a different order, can also read as drift. To see exactly what changed, open the assessment run and read its plan.

Some resources are managed somewhere else, or change on purpose. You can tell a workspace that they are never drift. On the workspace's Settings tab, list them under Not drift, one per line, and press Save health assessments. Each line is one of these:

you writewhich excepts
aws_instance.webevery instance of aws_instance.web
aws_instance.web[0]that instance only
aws_instance.*every aws_instance
module.app.aws_s3_bucket.logsthat resource in that module
module.legacy.*every resource in module.legacy, and in the modules inside it
aws_s3_bucket.logs.tagsonly the tags of aws_s3_bucket.logs
google_cloud_run_v2_service.api.template.containers.imagethat nested attribute, in every container

A * matches any part of one name, never across a dot. An attribute path takes no indices: it covers the attribute in every block that holds it.

An excepted resource is never reported as drift, even if it is deleted. An excepted attribute is removed before the change is compared, so a resource whose only change is to that attribute is not drift. A resource deleted or replaced outside Terraform is still drift even if you excepted one of its attributes, because the object itself moved. Exceptions apply after the rule above, as well as ignore_changes, so they can only take drift away.

A data source cannot be excepted, because it is never drift. A line that excepts everything is refused: turn health assessments off instead. A workspace can have up to 100 exceptions. Over the Terraform API, they are the workspace attribute drift-exceptions, a list of strings. Send the complete list each time, or [] to clear it.

On Terraform in the dashboard, the Drift / health column shows each workspace's latest assessment and when it ran. The verdict is the coloured label; a count and the state of your checks sit on lines of their own beneath it:

it sayswhich means
No driftthe last assessment found nothing changed and no failing checks
Drifted, then 3 resourcesthree resources changed outside Terraform in something your configuration sets
Checks failinga check block failed or errored, with or without drift
Checks pendinga check block has not reported a result yet
Assessment erroredthe assessment could not finish, so there is no conclusion
Awaiting first assessmentassessments are on and none has finished yet
Not enabledassessments are off for this workspace
Not assessablethe workspace cannot be assessed, for one of the reasons above

An assessment that could not finish, for example because the provider's credential was refused, reads Assessment errored and gives the reason. It never reads as "No drift".

The workspace's Health tab opens with a summary: the verdict, when the last assessment ran with a link to its run (so you can read the whole plan), and when the next one is due. Three tabs under it show the details:

  • Drift lists each drifted resource, by address, with the actions the plan wants to take;

  • Continuous validation lists every check of the latest assessment, failing ones first, each with its kind, its status and its message;

  • History lists earlier assessments, newest first. Filter them by outcome (drifted, no drift, errored, checks failing), and press Load older to read twenty more at a time. The filter works on the assessments loaded so far.

The tab you are on, the filter and how many assessments are loaded are in the address, so a link opens the same view.

Drift alerts go wherever you choose. There are three triggers:

triggersent when
assessment:driftedan assessment finds drift where the previous one did not, or the set of drifted resources changes
assessment:failedan assessment errors after one that did not
assessment:check_failurea check block starts failing

They fire on the change, not on every assessment. A workspace that stays drifted for a month sends one alert, not one a day.

Notify only on new drift is on by default, on the workspace's Settings tab. Turn it off to be sent assessment:drifted after every assessment that finds drift, as a reminder until the drift is dealt with. It changes only drift alerts. A failed assessment and a failing check are still sent once, when they start. Over the Terraform API, it is the workspace attribute drift-notify-only-new.

Under Settings → Notifications, a Slack, email or webhook channel can tick Drift was detected, A Terraform check started failing and A health assessment could not run, under Terraform drift and health. Each channel chooses for itself, so drift can go to one channel and not another. A channel set to Only these workspaces hears about only those. Microsoft Teams is not available for Terraform alerts yet, for the reason given on notifications. A webhook receives these as infra.assessment_drifted, infra.assessment_check_failure and infra.assessment_failed. Their data holds the counts and up to ten drifted addresses, never a value from the plan.

A workspace's notification configuration can carry the triggers above, using the generic destination, the same as run notifications. A configuration that does not list a trigger is not sent it. Each delivery is signed the same way as run notifications.

Every assessment is a plan-only run, billed in run-minutes at your Terraform rate. Each one counts for at least one minute. On Usage, Terraform minutes are split by what started each run, and health assessments have a line of their own, so you can see exactly what drift detection costs you. Health assessment runs do not send run notifications.