Terraform does not currently support "reusable functions" unless you write your own provider in another language, and so if you want to use function call syntax in particular then writing a utility provider containing the function you want would be the only way to achieve that. There's some documentation on provider-defined functions, but that's a pretty heavy lift just to get a simple custom template engine into Terraform.
However, there are some other potential ways to solve this which at least avoid treating every single replaceable token as a separate function call.
The built-in templatestring function
If it's not important to use this exact template syntax then probably the easiest path would be to use Terraform's own template syntax using the built-in templatestring function.
resource_groups = {
app_ops_rg = {
name = "rg-$${environment}-$${project}-$${regionalias}-$${applicationname}-app"
tags = {
module = "Test"
application = "Test"
}
}
}
name_template_vars = {
application_module_name = local.config_values.application_module_name
prod_subscription_id = local.config_values.prod_subscription_id
# ...
}
name = templatestring(rg.name, local.name_template_vars)
An important detail to watch for here is that it involves writing a string template using Terraform's own string template syntax, and so the template definition must use $${ instead of just ${ to escape the template interpolations at first, so that the string that you eventually pass to templatestring still has those template interpolation sequences intact. Symbols like environment are not defined in the main module scope, so if you write just ${environment} then you'll see a confusing error where Terraform misunderstands that as referring to an undeclared resource of type "environment".
Generalized template substitution
If using the {symbol} interpolation syntax is important to you then you can potentially write a reusable Terraform module that can render templates like that, returning the result as an output value. Of course this is not as ergonomic to use as a normal function call, but nonetheless avoids repeating the template substitution rules in multiple places.
variable "template" {
type = string
}
variable "symbols" {
type = map(string)
}
locals {
tokens = regexall("(?:\\{\\w+}|[^{]+|\\{)", var.template)
parts = tolist([
for token in local.tokens : (
startswith(token, "{") && length(token) > 1 ?
var.symbols[substr(token, 1, length(token)-2)] :
token
)
])
}
output "rendered" {
value = join("", local.parts)
}
The general approach here is to split the "template" string into a list of tokens, where each token is either a literal string or a template substitution. A template substitution is any token that starts with { and has at least two characters. For example, given the template string "Hello, {name}!" the tokens would be ["Hello, ", "{name}", "!"], where the second element meets the definitino of "template substitution" while the other two are just literals.
The expression for local.parts then visits each of the tokens and decides whether it's a substitution or a literal. If it's a substitution then it replaces it with a lookup into var.symbols.
The output value "rendered" then joins the transformed parts back together again into a single string to return.
If you place the above code in a shared module then you can use it like this:
module "resource_group_names" {
source = "../modules/render-template"
for_each = var.resource_groups
template = each.value.name
symbols = {
application_module_name = local.config_values.application_module_name
prod_subscription_id = local.config_values.prod_subscription_id
}
}
module.resource_group_names would then be a map of objects where the keys are the resource group keys (like "app_ops_rg" in your example) and the objects each have a rendered attribute containing the rendered resource group name. That is, module.resource_group_names["app_ops_rg"].rendered would contain the rendered name for that one resource group.