DevOps · Terraform

Terraform 1.9 New Features — A Complete Guide

Cross-variable input validation is the headline change in the Terraform 1.9 line, plus what shipped just before and after it: provider-defined functions and a maturing terraform test framework.

John Kihiu12 min read

Most Terraform release notes are a list of provider bugfixes with one or two things that actually change how you write configuration. The 1.9 line is one of the latter: the headline feature — validation blocks that can reference other input variables — fixes a gap that's been an open request since validation blocks were first added. It's a small syntax change with real practical value if you've ever tried to enforce "these two variables have to agree with each other" and given up because the language wouldn't let you.

Cross-variable validation

Before 1.9, a variable's validation block could only look at its own value. You could check that var.environment was one of a fixed set of strings, but you couldn't check that var.environment and var.instance_count made sense together — that production always requested at least three instances, say. The workaround was pushing that check into a precondition on a resource or output, which works but puts the error somewhere less obvious and further from the input that actually caused it.

Terraform 1.9 lets a variable's validation condition reference other variables in the same module, not just itself. That means you can put the check where the mistake is made, and get a plan-time error with a message you wrote instead of a downstream apply failure.

HCL
variable "environment" {
  type = string
}

variable "instance_count" {
  type = number
}

variable "instance_count" {
  type = number

  validation {
    condition = !(
      var.environment == "production" && var.instance_count < 3
    )
    error_message = "Production must run at least 3 instances."
  }
}
Only one variable declaration wins the merge

You cannot actually declare the same variable name twice in one module the way the snippet above implies for readability — that will error. In real configuration, write the validation block once, inside the variable it most naturally belongs to, and reference the other variable by name in the condition. I split it above only to make clear which variable is being validated against which.

The practical effect is that a whole category of "this combination of inputs doesn't make sense" bugs move from an apply-time failure, or worse a successful apply that leaves you in a bad state, to a plan-time validation error with a message you control. For a shared module used by other teams, that's the difference between a clear error and a support ticket.

Provider-defined functions

Terraform has always shipped a fixed set of built-in functions — string manipulation, collection operations, encoding helpers — and providers had no way to add their own. Around the 1.8 line, that changed: providers can now define their own functions, which show up in configuration namespaced under the provider, alongside the built-ins.

The practical use case is provider-specific logic that previously had to live in a separate script, a local-exec provisioner, or an external data source: parsing a provider's specific ID format, validating a region name against that provider's actual list of regions, or doing arithmetic specific to that provider's billing model. AWS, Google, and a handful of other providers have started shipping a small number of these; the list is still short but growing.

HCL
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.40.0"
    }
  }
}

locals {
  # Example shape only — check the provider's docs for the
  # actual function name and signature before using this.
  parsed_arn = provider::aws::arn_parse(var.role_arn)
}

output "account_id" {
  value = local.parsed_arn.account_id
}

The syntax is provider::<provider_name>::<function_name>(...). You don't need to configure anything extra to use one — if the provider version you've pinned ships the function, it's available in any expression in that module, same as a built-in.

terraform test keeps maturing

terraform test — the native testing framework built around .tftest.hcl files, run blocks, and assertions — isn't new to 1.9, but it kept picking up capability through this era of releases, in particular around mocking providers so a test can run without actually creating cloud resources. That matters for module authors: you can assert on planned values and outputs without paying for real infrastructure or needing live credentials in CI, and without the test run being at the mercy of a slow or flaky API.

HCL
run "instance_count_matches_environment" {
  command = plan

  variables {
    environment    = "production"
    instance_count = 3
  }

  assert {
    condition     = var.instance_count >= 3
    error_message = "Expected production to request at least 3 instances."
  }
}

run "rejects_understaffed_production" {
  command = plan

  variables {
    environment    = "production"
    instance_count = 1
  }

  expect_failures = [
    var.instance_count,
  ]
}

That second run block is the part worth noticing: once you have cross-variable validation in the module itself, terraform test can assert that the validation actually fires for a bad combination of inputs, not just that the happy path plans cleanly. Testing the failure path is normally the thing that gets skipped.

The removed block — recent, not new in 1.9

A change I see conflated with 1.9 is the declarative removed block, which lets you drop a resource from state without destroying the real infrastructure, as a config-file alternative to running terraform state rm by hand. It's genuinely useful alongside cross-variable validation because both are about making intent explicit in configuration rather than as one-off CLI commands your team has to remember to run. But it landed in a recent prior version of Terraform, not in the 1.9 line itself, and I'd rather say "a recent version" here than state a specific minor version I haven't re-verified against the changelog.

Verify before you cite a version number

Terraform's release cadence means minor-version attribution drifts in blog posts and internal docs faster than you'd expect. Before you put an exact "landed in 1.x" claim in a runbook or a PR description, check it against HashiCorp's own changelog for that release — don't copy it from a second-hand source, including this one.

Wrapping up

The theme across this stretch of Terraform releases is the same: push logic that used to live outside the configuration — in wrapper scripts, in tribal knowledge, in a wiki page about "combinations that don't work" — back into the configuration itself, where it's versioned, reviewed, and testable. Cross-variable validation is the clearest example: a rule that used to live in someone's head, or in a precondition buried three resources downstream, can now sit next to the variable it protects and fail at plan time with a message you wrote. None of this is dramatic. It's the kind of change that saves you an hour every few weeks instead of saving you a launch, which is exactly the kind of change that's worth adopting quietly rather than announcing loudly.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.