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?
| Tool | Best For | Team Size | Learning Curve |
|---|---|---|---|
| Crossplane | Kubernetes-native teams | Small → Large | Medium |
| Terraform | Multi-cloud, large orgs | Any | Low-Medium |
| Bicep | Azure-exclusive teams | Any | Low |
| Pulumi | Dev-heavy teams, testing focus | Small → Medium | Medium-High |
| CUE | Config-heavy platforms, validation | Medium → Large | High |
The Practical Choice in 2025
Here’s what teams are actually doing:
If you’re Azure-only: Bicep wins. It’s purpose-built, compiles to ARM, and Microsoft is investing heavily.
If you’re multi-cloud: Terraform is still the safest bet. The ecosystem is massive and it “just works.”
If you want better testing: Pulumi is becoming the standard for teams with mature CI/CD practices.
If you need strict config validation: CUE is emerging as the answer for large platforms with complex governance.
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:
- Terraform + Pulumi to lead adoption due to existing momentum
- Bicep to become the default for Azure organizations
- CUE to become standard in large enterprises with strict governance
- Crossplane to dominate the Kubernetes-first DevOps space
- 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:
- Evaluate one alternative with a non-critical workload
- Compare time-to-value: How long to define, validate, and deploy the same infrastructure?
- Assess your team: Do they code, or do they prefer declarative simplicity?
- 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.
