Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 8 min read

Provider-Defined Functions: The Terraform Feature That Kills Your Locals Sprawl

Terraform's built-in functions can't do everything, so people build grotesque locals to parse ARNs and encode JWTs. Provider-defined functions fix that. Here's how.

  • #terraform
  • #functions
  • #providers
  • #hcl
  • #expressions
  • #clean-code

Every mature Terraform codebase has them: the cursed locals block. The one that splits an ARN with a chain of split, element, and regex calls to pull out the account ID. The one that base64-encodes a string and then JSON-encodes it and then templates it. The one with a comment that just says # don't touch this, it works. These exist because Terraform’s built-in function set is fixed, and when you need something it doesn’t have, you contort the functions you do have until the result is unreadable.

Provider-defined functions are the feature that makes most of that contortion unnecessary. Providers can now ship their own functions, callable directly in your HCL. It’s a small-sounding capability with an outsized effect on how clean your configuration can be.

The old world: built-ins only

Before this, the entire universe of functions was whatever HashiCorp baked into the language — length, lookup, cidrsubnet, jsonencode, and friends. Useful, but fixed. Need to parse a provider-specific identifier? You reverse-engineered its format and reconstructed it with string surgery:

locals {
  # Parse account ID out of an ARN the hard way
  arn_parts  = split(":", aws_iam_role.app.arn)
  account_id = local.arn_parts[4]
  # ...and pray the ARN format never has an edge case
}

This works until it doesn’t — an ARN with a path, a partition that isn’t aws, a resource type with extra colons — and then your hand-rolled parser quietly returns the wrong substring.

The new world: ask the provider

Providers understand their own formats. So now the AWS provider can expose a function that parses an ARN correctly, edge cases and all. You call it through the provider:: namespace:

locals {
  parsed     = provider::aws::arn_parse(aws_iam_role.app.arn)
  account_id = local.parsed.account_id
  region     = local.parsed.region
  resource   = local.parsed.resource
}

One call, a structured result, and the parsing logic is maintained by the people who define the format. Your locals block went from a fragile string-surgery exhibit to a single readable line.

The namespace tells you where it came from

The syntax is deliberately explicit:

provider::<provider_name>::<function_name>(args...)

That verbosity is a feature. When you read provider::aws::arn_parse(...), you know immediately this isn’t a built-in — it’s coming from the AWS provider, and you can look it up in that provider’s docs. No guessing whether a function is core Terraform or something a module author monkey-patched in.

Where these earn their keep

The functions worth reaching for cluster around things providers know and HCL doesn’t:

  • Identifier parsing/building — ARNs, resource IDs, fully-qualified names. Stop hand-splitting them.
  • Encoding and crypto helpers — provider-specific token formats, signing, encoding schemes that are annoying to assemble from primitives.
  • Validation and normalization — “is this a well-formed X for this provider?” answered authoritatively.

A concrete cleanup. This kind of nested-built-in horror:

locals {
  config_blob = base64encode(jsonencode({
    endpoint = var.endpoint
    token    = var.token
  }))
}

…stays simple, but the parsing counterpart — taking a provider-emitted blob apart — is exactly where a provider function replaces a pile of try(jsondecode(base64decode(...)), {}) defensive nesting with one trustworthy call.

They compose with everything else

Provider-defined functions are just expressions, so they slot into for_each, conditionals, and dynamic blocks like any other function:

resource "aws_iam_role_policy" "scoped" {
  for_each = toset(var.role_arns)

  name = "scope-${provider::aws::arn_parse(each.value).account_id}"
  role = aws_iam_role.app.id
  # ...
}

You’re naming a policy by the account ID parsed cleanly out of each ARN in a set — the kind of thing that used to require a precomputed map of mangled strings in a locals block three screens tall.

Caveats before you sprinkle them everywhere

  • The provider must ship the function and be configured. These come from the provider plugin, so you need a recent enough provider version, and the provider must be present in your config. Check the provider’s docs for which functions exist — the set varies a lot between providers.
  • Version-gate them. A function that exists in provider 5.70 won’t exist in 5.40. If your module is shared, your required_providers constraint needs to guarantee a version new enough to have the function, or consumers get a cryptic “function not found” error.
  • Don’t over-abstract. A two-line built-in expression that’s perfectly clear doesn’t need replacing just because a provider function exists. Reach for these when they remove fragility, not to chase novelty.
  • Test the edge cases anyway. A provider function parsing ARNs is more correct than your hand-rolled splitter, but you should still have a test asserting your config does the right thing with a weird input.

The net effect

The reason I care about this feature isn’t any single function — it’s what it does to readability across a codebase. The grotesque locals blocks were never about logic that needed to be grotesque; they were workarounds for a fixed function set. Hand that work to the provider that actually understands the data, and a whole genre of “don’t touch this, it works” comments disappears.

Function-heavy expressions are also where subtle bugs hide — an off-by-one in a parsed field, a version constraint that doesn’t actually guarantee the function exists — and they read as correct at a glance. Running these refactors through a code review workflow catches the version-gating misses. For more on writing Terraform that the next engineer can actually read, see our other Terraform guides.

Provider-defined functions and their availability vary by provider and version. Verify against the specific provider’s current documentation before depending on them.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.