The YAML Era is Ending—But What’s Actually Replacing It?

YAML once saved us from XML hell. It was readable, simple, and became the de facto standard for everything from Kubernetes to CI/CD pipelines. But in 2025, that simplicity has become a liability.

The problems are real:

  • Indentation errors that take hours to debug
  • No type safety until you deploy
  • Zero reusability (endless copy-paste templates)
  • Impossible merge conflicts in distributed teams
  • No validation until runtime

The question teams are asking now isn’t “Should we use YAML?” It’s “What are we moving to next?”


The Real Alternatives: Beyond Hype

Rather than vague promises, I’ll show the exact same infrastructure described in four different languages.

We’ll deploy a simple but realistic scenario:

  • One Azure Container Registry (ACR) to store Docker images
  • One Azure Key Vault to manage secrets
  • One storage account with encryption enabled

Let me show how each approach handles this.


The Setup: Deploying Azure Container Registry + Key Vault + Storage

Option 1: Raw YAML (Kubernetes CRDs + Crossplane)

Crossplane declares cloud resources as Kubernetes CustomResourceDefinitions with more structure than raw YAML.

apiVersion: v1
kind: Namespace
metadata:
  name: infrastructure
---
apiVersion: azure.crossplane.io/v1
kind: ResourceGroup
metadata:
  name: my-rg
spec:
  forProvider:
    location: eastus
  providerConfigRef:
    name: azure-provider
---
apiVersion: containerregistry.azure.crossplane.io/v1
kind: Registry
metadata:
  name: my-acr
spec:
  forProvider:
    resourceGroupName: my-rg
    location: eastus
    adminUserEnabled: true
    sku: Basic
  providerConfigRef:
    name: azure-provider
---
apiVersion: keyvault.azure.crossplane.io/v1
kind: Vault
metadata:
  name: my-vault
spec:
  forProvider:
    resourceGroupName: my-rg
    location: eastus
    enabledForDeployment: true
    enabledForDiskEncryption: true
    enabledForTemplateDeployment: true
    tenantId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    sku:
      family: A
      name: standard
  providerConfigRef:
    name: azure-provider
---
apiVersion: storage.azure.crossplane.io/v1
kind: Account
metadata:
  name: my-storage
spec:
  forProvider:
    resourceGroupName: my-rg
    location: eastus
    kind: StorageV2
    sku:
      name: Standard_LRS
    accessTier: Hot
    encryption:
      enabled: true
      services:
        blob:
          enabled: true
        file:
          enabled: true
  providerConfigRef:
    name: azure-provider

Pros:

  • Native Kubernetes integration
  • GitOps-friendly
  • Familiar YAML syntax

Cons:

  • Still indentation-based
  • No validation until deployment
  • No type checking
  • Difficult to compose complex resources
  • No built-in loops or conditionals

Option 2: Terraform (HCL - HashiCorp Configuration Language)

Terraform is widely adopted and uses HCL, a structured configuration language with programming concepts.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "main" {
  name     = "my-rg"
  location = "East US"
}

resource "azurerm_container_registry" "acr" {
  name                = "myacr"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  sku                 = "Basic"
  admin_enabled       = true
}

resource "azurerm_key_vault" "vault" {
  name                       = "my-vault"
  location                   = azurerm_resource_group.main.location
  resource_group_name        = azurerm_resource_group.main.name
  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "standard"
  enabled_for_deployment     = true
  enabled_for_disk_encryption = true
  enabled_for_template_deployment = true
}

resource "azurerm_storage_account" "storage" {
  name                     = "mystorageacct"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  access_tier              = "Hot"

  https_traffic_only_enabled       = true
  min_tls_version                  = "TLS1_2"
  infrastructure_encryption_enabled = true
}

resource "azurerm_storage_account_blob_properties" "storage_blob" {
  storage_account_id = azurerm_storage_account.storage.id
  last_access_time_enabled = true
}

data "azurerm_client_config" "current" {}

output "acr_login_server" {
  value = azurerm_container_registry.acr.login_server
}

Pros:

  • Powerful state management
  • Modules for reusability
  • Large ecosystem
  • Easy to reference resources
  • Support for variables, locals, and logic

Cons:

  • HCL is Terraform-specific (limited portability)
  • Verbose for simple configurations
  • State file management adds complexity
  • Debugging can be cryptic

Option 3: Bicep (Microsoft’s Infrastructure-as-Code Language)

Bicep is Microsoft’s Infrastructure-as-Code language for Azure. It compiles to ARM templates with improved readability and is Azure-native.

param location string = 'eastus'
param environment string = 'dev'

var resourceGroupName = 'my-rg'
var acrName = 'myacr${uniqueString(resourceGroup().id)}'
var vaultName = 'myvault${uniqueString(resourceGroup().id)}'
var storageAccountName = 'mystg${uniqueString(resourceGroup().id)}'

resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
  name: acrName
  location: location
  sku: {
    name: 'Basic'
  }
  properties: {
    adminUserEnabled: true
    publicNetworkAccess: 'Enabled'
  }
  tags: {
    environment: environment
  }
}

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: vaultName
  location: location
  properties: {
    enabledForDeployment: true
    enabledForDiskEncryption: true
    enabledForTemplateDeployment: true
    tenantId: subscription().tenantId
    sku: {
      family: 'A'
      name: 'standard'
    }
    accessPolicies: []
  }
  tags: {
    environment: environment
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    accessTier: 'Hot'
    allowBlobPublicAccess: false
    httpsTrafficOnlyEnabled: true
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
  tags: {
    environment: environment
  }
}

resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: storageAccount
  name: 'default'
}

output acrLoginServer string = containerRegistry.properties.loginServer
output keyVaultUri string = keyVault.properties.vaultUri
output storageAccountId string = storageAccount.id

Pros:

  • Azure-native, purpose-built
  • Clean, readable syntax
  • Strong typing
  • Excellent Azure documentation
  • Modules for composition
  • Compiles to ARM templates (no external runtime)

Cons:

  • Azure-only (not multi-cloud)
  • Smaller community than Terraform
  • Language is relatively young

Option 4: Pulumi (Real Programming Languages)

Pulumi uses Python, TypeScript, Go, or C# to define infrastructure using a code-first approach.

import pulumi
import pulumi_azure_native as azure

config = pulumi.Config()
environment = config.get('environment') or 'dev'
location = config.get('location') or 'eastus'

# Create a resource group
resource_group = azure.resources.ResourceGroup(
    'my-rg',
    resource_group_name='my-rg',
    location=location,
    tags={'environment': environment}
)

# Create container registry
acr = azure.containerregistry.Registry(
    'my-acr',
    resource_group_name=resource_group.name,
    registry_name='myacr',
    location=location,
    sku=azure.containerregistry.SkuArgs(name='Basic'),
    properties=azure.containerregistry.RegistryPropertiesArgs(
        admin_user_enabled=True,
        public_network_access='Enabled'
    ),
    tags={'environment': environment}
)

# Create Key Vault
vault = azure.keyvault.Vault(
    'my-vault',
    resource_group_name=resource_group.name,
    vault_name='myvault',
    location=location,
    properties=azure.keyvault.VaultPropertiesArgs(
        enabled_for_deployment=True,
        enabled_for_disk_encryption=True,
        enabled_for_template_deployment=True,
        tenant_id=azure.core.get_client_config().then(lambda cfg: cfg.tenant_id),
        sku=azure.keyvault.SkuArgs(family='A', name='standard'),
        access_policies=[]
    ),
    tags={'environment': environment}
)

# Create storage account
storage = azure.storage.StorageAccount(
    'my-storage',
    resource_group_name=resource_group.name,
    account_name='mystg',
    location=location,
    kind='StorageV2',
    sku=azure.storage.SkuArgs(name='Standard_LRS'),
    access_tier='Hot',
    https_traffic_only_enabled=True,
    minimum_tls_version='TLS1_2',
    tags={'environment': environment}
)

# Create blob services (child resource)
blob_services = azure.storage.BlobServiceProperties(
    'blob-services',
    resource_group_name=resource_group.name,
    account_name=storage.name,
    blob_services_name='default'
)

# Export outputs
pulumi.export('acr_login_server', acr.login_server)
pulumi.export('key_vault_uri', vault.properties.vault_uri)
pulumi.export('storage_account_id', storage.id)

Pros:

  • Use your favorite programming language
  • Real OOP, loops, conditionals, functions
  • Strong typing (especially TypeScript/Go)
  • Easy testing and validation
  • Unified multi-cloud approach
  • Excellent IDE support

Cons:

  • Larger learning curve for ops-focused teams
  • Generated state (not as transparent as Terraform)
  • Less mature for certain edge cases
  • Community smaller than Terraform

Option 5: CUE (Configuration Unification Engine)

CUE is a language designed for configuration that bridges declarative and functional programming.

package main

import "encoding/json"

let location = "eastus"
let environment = "dev"
let uniqueId = "abc123"  // In real usage, this would be generated

// Define a reusable schema for tags
#Tags: {
    environment: string
}

// Container Registry
containerRegistry: {
    apiVersion: "2025-07-01"
    type: "Microsoft.ContainerRegistry/registries"
    name: "myacr\(uniqueId)"
    location: location
    sku: {
        name: "Basic"
    }
    properties: {
        adminUserEnabled: true
        publicNetworkAccess: "Enabled"
    }
    tags: {
        environment: environment
    }
}

// Key Vault
keyVault: {
    apiVersion: "2025-07-01"
    type: "Microsoft.KeyVault/vaults"
    name: "myvault\(uniqueId)"
    location: location
    properties: {
        enabledForDeployment: true
        enabledForDiskEncryption: true
        enabledForTemplateDeployment: true
        tenantId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        sku: {
            family: "A"
            name: "standard"
        }
        accessPolicies: []
    }
    tags: {
        environment: environment
    }
}

// Storage Account
storageAccount: {
    apiVersion: "2025-01-01"
    type: "Microsoft.Storage/storageAccounts"
    name: "mystg\(uniqueId)"
    location: location
    kind: "StorageV2"
    sku: {
        name: "Standard_LRS"
    }
    properties: {
        accessTier: "Hot"
        allowBlobPublicAccess: false
        httpsTrafficOnlyEnabled: true
        minimumTlsVersion: "TLS1_2"
    }
    tags: {
        environment: environment
    }
}

// Validate that all resources have the required tags
@validation("all resources must have environment tag")
for resource in [containerRegistry, keyVault, storageAccount] {
    resource.tags.environment != _|_ // Fail if environment tag is missing
}

Pros:

  • Purpose-built for configuration
  • Schema validation built-in
  • Defaults and constraints
  • Can output to YAML, JSON, or Terraform
  • Functional approach prevents copy-paste errors
  • Excellent for policy enforcement

Cons:

  • Very young ecosystem
  • Smaller community
  • Steeper learning curve
  • Limited cloud provider integrations

Real Talk: Which One Should You Actually Use?

ToolBest ForTeam SizeLearning Curve
CrossplaneKubernetes-native teamsSmall → LargeMedium
TerraformMulti-cloud, large orgsAnyLow-Medium
BicepAzure-exclusive teamsAnyLow
PulumiDev-heavy teams, testing focusSmall → MediumMedium-High
CUEConfig-heavy platforms, validationMedium → LargeHigh

The Practical Choice in 2025

Here’s what teams are actually doing:

  1. If you’re Azure-only: Bicep wins. It’s purpose-built, compiles to ARM, and Microsoft is investing heavily.

  2. If you’re multi-cloud: Terraform is still the safest bet. The ecosystem is massive and it “just works.”

  3. If you want better testing: Pulumi is becoming the standard for teams with mature CI/CD practices.

  4. If you need strict config validation: CUE is emerging as the answer for large platforms with complex governance.

  5. If you’re Kubernetes-first: Crossplane makes sense if you’re already running a control plane and want GitOps for cloud resources.


What’s Actually Killing YAML

It’s not that YAML is going away, it’s that the problems it creates at scale are now unbearable:

  • Merge conflicts in 200-line YAML files are killing productivity
  • Type safety gaps cause runtime disasters
  • No way to validate “the whole picture” before deployment
  • Copy-paste errors from templates cascade through production

The next generation of IaC tools addresses these with:

  • Type safety - Catch errors at parse time, not deploy time
  • Composition - Real modules, not template copy-paste
  • Testability - Validate infrastructure before deployment
  • IDE support - Autocomplete, linting, type hints
  • Real programming - Loops, functions, conditionals

The Convergence Happening Now

By 2026, expect:

  1. Terraform + Pulumi to lead adoption due to existing momentum
  2. Bicep to become the default for Azure organizations
  3. CUE to become standard in large enterprises with strict governance
  4. Crossplane to dominate the Kubernetes-first DevOps space
  5. Raw YAML to become a fallback option rather than a first choice

The diversity of tools reflects specialization. Each addresses specific problems that YAML cannot.


What You Should Do Right Now

If your infrastructure is still defined in 300-line YAML files:

  1. Evaluate one alternative with a non-critical workload
  2. Compare time-to-value: How long to define, validate, and deploy the same infrastructure?
  3. Assess your team: Do they code, or do they prefer declarative simplicity?
  4. Pick the tool that reduces your pain - not the one with the best marketing

The best infrastructure tool is the one that works for your team, your cloud provider, and your deployment patterns.

YAML didn’t fail because it was flawed. It succeeded widely, and now the industry understands its limitations at scale.

The post-YAML era is here.