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.70won’t exist in5.40. If your module is shared, yourrequired_providersconstraint 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.
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.