DEV Community

Cover image for Terraform 1.15 on Azure: New Features Explained with Real Examples
Mikael Krief
Mikael Krief

Posted on

Terraform 1.15 on Azure: New Features Explained with Real Examples

Terraform 1.15 introduces a small set of language changes, but several of them are immediately useful for Azure infrastructure teams. The release adds dynamic module sources and versions, deprecation metadata for variables and outputs, stricter output typing, more precise type conversion, backend validation improvements, and native Windows ARM64 support.[1][2]

For Azure users, these changes matter because they improve how reusable modules are published, how breaking changes are signaled, and how Terraform configurations fail earlier during validation. Microsoft’s Azure Terraform documentation continues to emphasize common patterns such as resource groups, virtual machines, AKS, Key Vault, Application Gateway, SQL Database, Front Door, private endpoints, and NAT Gateway, which make good examples for these new features.[3]

What changed in Terraform 1.15

The most important Terraform 1.15 changes are:

  • Dynamic module source and version values using variables and locals, as long as the values are available during configuration loading.[1][2]
  • deprecated messages on variable and output blocks, so module authors can warn users before a breaking change lands.[1][2]
  • A new convert() function for more explicit conversion to complex types.[2]
  • A type argument on output blocks, bringing outputs closer to variables in terms of contract clarity and validation.[2]
  • terraform validate improvements, including validation of backend blocks.[2]
  • Native Windows ARM64 binaries, which improve the local developer experience on supported Windows devices.[1]

1. Dynamic module sources and versions

Before Terraform 1.15, module source and version arguments were intentionally rigid. Terraform 1.15 now allows module authors and platform teams to use variables and locals for those values when they are known early enough, which makes environment-specific composition easier.[1][2]

On Azure, this is useful when a team standardizes around Azure Verified Modules, but wants to choose a different module version for development, staging, and production. Azure promotes Terraform modules and Azure Verified Modules as a way to package repeatable infrastructure patterns, so this feature fits naturally into enterprise Azure landing zones.[4][5]

Azure example: choosing a Key Vault module version per environment

variable "environment" {
  type    = string
  default = "dev"
  const   = true
}

locals {
  key_vault_module_version = {
    dev  = "0.9.0"
    prod = "1.2.0"
  }

  key_vault_version = local.key_vault_module_version[var.environment]
}

module "key_vault" {
  source  = "Azure/avm-res-keyvault-vault/azurerm"
  version = local.key_vault_version

  name                = "kv-platform-${var.environment}-001"
  location            = azurerm_resource_group.platform.location
  resource_group_name = azurerm_resource_group.platform.name
  tenant_id           = data.azurerm_client_config.current.tenant_id
}
Enter fullscreen mode Exit fullscreen mode

This pattern is helpful when a platform team wants to pin conservative versions in production while allowing faster iteration in lower environments. It also reduces duplicated root modules that differ only by a module version string.[1][2][5]

Azure example: switching an AKS module source

variable "module_channel" {
  type    = string
  default = "stable"
  const   = true
}

locals {
  aks_sources = {
    stable = "git::https://github.com/Azure/terraform-azure-modules.git//modules/aks?ref=v1.0.0"
    next   = "git::https://github.com/Azure/terraform-azure-modules.git//modules/aks?ref=v1.1.0-beta"
  }
}

module "aks" {
  source = local.aks_sources[var.module_channel]

  resource_group_name = azurerm_resource_group.platform.name
  location            = azurerm_resource_group.platform.location
}
Enter fullscreen mode Exit fullscreen mode

For Azure teams, this creates a cleaner release process for shared AKS, networking, or Key Vault modules. The trade-off is that the chosen values must still be deterministic during init-time configuration loading, so this is not a replacement for fully runtime-driven composition.[1][2]

2. Variable and output deprecation

Terraform 1.15 lets module authors attach a deprecated message to variables and outputs. Terraform then surfaces warnings during commands such as validate, plan, and apply, which gives consumers time to migrate before a major version bump.[1][2]

This is especially valuable on Azure, where internal modules often evolve from basic resources to more opinionated patterns. For example, a storage account module may start with a simple location input and later move to a shared naming and placement model based on a landing zone object.[3][5]

Azure example: deprecating a flat location variable in a resource group module

variable "location" {
  type       = string
  default    = null
  deprecated = "Use var.platform.location instead. This input will be removed in v2.0."
}

variable "platform" {
  type = object({
    location = string
    tags     = map(string)
  })
}

resource "azurerm_resource_group" "this" {
  name     = "rg-app-prod-001"
  location = coalesce(var.location, var.platform.location)
  tags     = var.platform.tags
}
Enter fullscreen mode Exit fullscreen mode

This lets the module stay backward compatible while steering consumers toward the new API. That is much safer than removing the old variable immediately, especially for heavily reused Azure resource group and networking modules.[1][2]

Azure example: deprecating a legacy output

output "subnet_id" {
  value      = azurerm_subnet.app.id
  deprecated = "Use output.private_subnet_id instead. subnet_id will be removed in v3.0."
}

output "private_subnet_id" {
  value = azurerm_subnet.app.id
  type  = string
}
Enter fullscreen mode Exit fullscreen mode

In Azure environments, this is useful when a network module evolves from a generic subnet model to more specific outputs such as private_subnet_id, aks_subnet_id, or appgw_subnet_id. Consumers get a warning without an immediate outage in their downstream Terraform code.[1][2][3]

3. Typed outputs

Terraform 1.15 adds a type argument to output blocks. That makes outputs clearer to consumers and allows terraform validate to fail when a module returns a value that does not match the declared contract.[2]

This is particularly helpful in Azure modules that return structured data for multiple resources, such as a virtual network module returning subnet IDs, NSG IDs, and route table IDs. Azure environments often rely on chaining modules together, so clearer output contracts improve maintainability.[3][5]

Azure example: typed output for a virtual network module

output "network" {
  type = object({
    vnet_id            = string
    app_subnet_id      = string
    private_endpoints  = list(string)
  })

  value = {
    vnet_id           = azurerm_virtual_network.this.id
    app_subnet_id     = azurerm_subnet.app.id
    private_endpoints = [for pe in azurerm_private_endpoint.this : pe.id]
  }
}
Enter fullscreen mode Exit fullscreen mode

This gives downstream modules a stronger contract than a loosely documented output map. For example, an Application Gateway or private endpoint module can depend on an expected object shape rather than parsing ad hoc strings or nested maps.[2][3]

4. Precise type conversion with convert()

Terraform 1.15 adds the convert() function to coerce a value into a target type more explicitly. This helps avoid subtle mismatches when values look similar but are not represented with the same underlying type.[2]

Azure modules frequently accept complex inputs, especially around tags, subnet definitions, NSG rules, or diagnostic settings. In those situations, explicit conversion can make module boundaries safer and easier to debug.[3][5]

Azure example: normalizing subnet configuration

variable "subnets" {
  type = any
}

locals {
  normalized_subnets = convert(var.subnets, map(object({
    address_prefixes = list(string)
    nsg_name         = string
  })))
}

resource "azurerm_subnet" "this" {
  for_each             = local.normalized_subnets
  name                 = each.key
  resource_group_name  = azurerm_resource_group.network.name
  virtual_network_name = azurerm_virtual_network.network.name
  address_prefixes     = each.value.address_prefixes
}
Enter fullscreen mode Exit fullscreen mode

This pattern is useful when different teams supply input through CI variables, JSON, or wrapper modules. Instead of relying on loose implicit typing, the module can enforce the exact shape it expects before Azure resources are created.[2]

5. Better validation and backend checks

Terraform 1.15 improves terraform validate, including checks for backend configuration. That gives teams earlier feedback when state configuration is wrong, instead of discovering the issue later in a deployment workflow.[2]

On Azure, remote state is often stored in a storage account and blob container. Earlier validation is valuable because state configuration is foundational to collaborative workflows, and Microsoft’s Terraform guidance includes storage-backed operational patterns as part of Azure infrastructure management.[6][3]

Azure example: validating an Azure-backed remote state design

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate-prod-001"
    storage_account_name = "sttfstateprod001"
    container_name       = "tfstate"
    key                  = "networking/prod.tfstate"
  }
}
Enter fullscreen mode Exit fullscreen mode

This feature does not change how the AzureRM backend is written, but it helps surface backend mistakes earlier in the workflow. That is particularly useful for shared platform repositories where a backend typo can block multiple teams.[2]

6. Windows ARM64 support

HashiCorp also added native Windows ARM64 support in Terraform 1.15. This mainly affects local tooling, but it matters for developers using ARM-based Windows laptops or build agents.[1]

For Azure-focused teams, that change does not alter Azure resource syntax, yet it can simplify local development when engineers test infrastructure code for services like AKS, Key Vault, SQL Database, or virtual machines from ARM64 Windows environments. Microsoft’s Azure Terraform documentation spans all of these services, which means the feature improves the developer platform rather than the Azure provider model itself.[1][3]

Azure-focused upgrade advice

For Azure module authors, the most practical Terraform 1.15 upgrades are to adopt deprecation messages, add explicit output types, and use convert() on complex interfaces first. Those changes improve safety without forcing a redesign of existing Azure resource code.[1][2]

Dynamic module sources and versions are powerful, but they should be introduced carefully in platform repositories that already have strict version pinning and release automation. The strongest use case is a centrally managed Azure module ecosystem where environments intentionally consume different tested versions of the same module.[1][2][5]

Resources

Top comments (0)