A Breakup Letter to YAML
A Breakup Letter to the Configuration Language That Broke My Heart (And My Deployments)

This is the fourth article in my infrastructure series. If you missed the previous episodes of my descent into madness, check out Why I Sacrificed a Goat to AWS Gods, From Rant to Reality: Why I Started Light Cloud, and The $4,847 Bill That Changed My Life.
Dear YAML,
We need to talk.
I know we've had some good times together. Remember when you replaced XML and everyone thought you were so clean and readable? Those were simpler days. Back when a deployment meant copying files to /var/www/html and calling it a day.
But somewhere along the way, our relationship became... complicated.
It Started So Innocently
Remember our first date? A simple Docker Compose file:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
So elegant! So readable! My heart fluttered. "Finally," I thought, "a configuration language that doesn't look like it was designed by someone who hates humanity."
I was so naive.
The Red Flags I Should Have Seen
Looking back, there were warning signs. Like that time you made me spend 3 hours debugging a deployment failure because I used tabs instead of spaces. Or when you insisted that "no" and "false" and false are three different things. WHO DOES THAT, YAML?
But I ignored the red flags because you promised me simplicity. You whispered sweet nothings about being "human-readable" and "easy to understand." You lying, indentation-obsessed monster.
When Things Got Serious (AKA Kubernetes)
Then Kubernetes entered our lives, and everything changed. Suddenly, you weren't just handling simple key-value pairs. You were orchestrating the fate of entire applications. And you... you became a different YAML. A darker YAML.
This is what you made me write to deploy a simple web app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-simple-app
labels:
app: my-simple-app
spec:
replicas: 3
selector:
matchLabels:
app: my-simple-app
template:
metadata:
labels:
app: my-simple-app
spec:
containers:
- name: my-simple-app
image: my-simple-app:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: my-simple-app-service
spec:
selector:
app: my-simple-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-simple-app-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-simple-app-service
port:
number: 80
YAML, this is 60 lines of configuration to deploy a Node.js app that serves "Hello World." SIXTY LINES. For comparison, the actual application code is 4 lines:
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(3000);
I'm writing 15 times more configuration than actual code. This is not what you promised me, YAML.
The Great Helm Chart Incident of 2024
Things got worse when we started using Helm. Suddenly you weren't just YAML anymore. You were YAML with template variables. You became some kind of YAML-Go-template hybrid abomination:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class")) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
YAML, what have you become? This isn't configuration anymore. This is a programming language pretending to be configuration. And not even a good programming language! It's like someone took the worst parts of templating engines and mashed them together with your indentation obsession.
The Debugging Nightmares
But the real betrayal? The debugging experience. When something goes wrong, you give me error messages like:
error converting YAML to JSON: yaml: line 47: found character that cannot start any token
Line 47? CHARACTER THAT CANNOT START ANY TOKEN? What does that even mean? Which character? What token were you expecting? Why can't you just tell me I have a typo like a normal language?
I've spent more time counting spaces in YAML files than I have optimizing actual application performance. Let that sink in.
The Values.yaml Inception
And don't get me started on values.yaml files. You made me create YAML files to configure other YAML files. It's YAML all the way down, like some kind of configuration language inception nightmare:
# values.yaml-breakup
replicaCount: 3
image:
repository: my-app
pullPolicy: IfNotPresent
tag: "latest"
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
resources: {}
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
I need a PhD in YAML archaeology just to understand what half of these fields do. And if I want to enable HTTPS? I have to navigate through this maze of nested configurations like I'm defusing a bomb.
The Final Straw: ArgoCD
The relationship truly hit rock bottom when we started using ArgoCD for GitOps. Now I had to manage YAML files that manage other YAML files that deploy applications defined in more YAML files.
Here's what I needed to deploy a simple app with ArgoCD:
- Application YAML (defines what to deploy)
- Deployment YAML (defines how to deploy)
- Service YAML (defines networking)
- Ingress YAML (defines external access)
- ConfigMap YAML (defines configuration)
- Secret YAML (defines secrets)
- values.yaml (defines Helm variables)
- Chart.yaml (defines Helm metadata)
Eight different YAML files. For one application. That serves "Hello World."
My git repository looked like a YAML graveyard. Hundreds of nearly identical files, differing only in tiny details that somehow break everything if you get them wrong.
What We've Learned About Each Other
YAML, through our toxic relationship, I've learned some hard truths:
- You're not actually human-readable when you reach any meaningful complexity
- Your indentation sensitivity is a feature nobody asked for
- You make simple things complex and complex things impossible
- You've created an entire industry of tools just to manage you (kustomize, helm, jsonnet)
- You're the reason developers are afraid of infrastructure
And I think I've finally figured out why our relationship never worked: You were never meant to be a programming language, but somehow you became one.
The Light Cloud Alternative: Visual Infrastructure Blocks
This is why we built Light Cloud's visual infrastructure blocks. Instead of wrestling with 60 lines of YAML, our users just drag and drop:
- App Block (connects to your git repo)
- Database Block (PostgreSQL, MySQL, or MongoDB)
- Cache Block (Redis with automatic clustering)
- Load Balancer Block (with automatic SSL)
Click, connect, deploy. No YAML in sight.
Our ICE (Integrated Cloud Environment) generates the infrastructure automatically. Want to see what's happening under the hood? Sure, we'll show you the generated Terraform and Kubernetes configs. But you never have to touch them.
One of our beta users deployed a production-ready e-commerce platform in 15 minutes. No YAML files were harmed in the making of this deployment.
It's Not Me, It's You
So YAML, this is goodbye. It's not me, it's definitely you.
You promised simplicity but delivered complexity. You promised readability but gave us hieroglyphics. You turned configuration into a full-time job.
But I want you to know, I don't hate you. You were a necessary step in our evolution. You taught us what we DON'T want in infrastructure tooling:
- Configuration shouldn't be harder than coding
- Deployment shouldn't require a specialty degree
- Infrastructure shouldn't be a full-time job
To My Fellow YAML Survivors
If you're reading this and nodding along, you're not alone. We've all been traumatized by indentation errors at 2 AM. We've all spent hours debugging why enabled: "false" doesn't work the same as enabled: false
But there's hope. The future of infrastructure is visual, intuitive, and YAML-free.
At Light Cloud, we're building that future. Come join us.
P.S. YAML, if you're reading this, please fix your error messages. "found character that cannot start any token" is not helpful feedback. Neither is "did not find expected key" when the key is literally right there.
P.P.S. To everyone who's about to comment "but YAML is simple once you understand it" - that's exactly the problem. I shouldn't need to "understand" a configuration format. It should just work.
Ready to break up with YAML too? Try Light Cloud's visual infrastructure blocks and deploy without the pain. Your future self will thank you.
