Post

YAML Infrastructure as Code Tools: A Comprehensive Guide

YAML Infrastructure as Code Tools

YAML (YAML Ain’t Markup Language) has become the de facto standard for defining infrastructure as code due to its human-readable syntax and simple structure. Unlike domain-specific languages or programming languages, YAML offers a declarative approach that’s easy to learn and widely supported across the DevOps ecosystem.

This guide explores the most popular YAML-based Infrastructure as Code tools, their use cases, and best practices.

Why YAML for Infrastructure as Code?

Advantages of YAML

Human-Readable: YAML’s clean syntax makes it easy for both developers and operations teams to read and write infrastructure definitions.

Language-Agnostic: YAML files can be processed by any programming language, making them universally compatible.

Declarative: YAML naturally supports a declarative approach where you specify what you want, not how to achieve it.

Version Control Friendly: Plain text format works seamlessly with Git and other version control systems.

Wide Adoption: Most modern IaC tools support YAML as a primary or alternative format.

Challenges with YAML

Indentation Sensitivity: YAML relies on spaces for structure, making it prone to indentation errors.

Limited Logic: YAML lacks built-in programming constructs like loops and conditionals (though some tools extend it).

Verbose for Complex Scenarios: Large infrastructure definitions can become unwieldy and repetitive.

No Type Safety: YAML doesn’t provide compile-time type checking.


1. Ansible

Type: Configuration Management & Orchestration
Primary Use: Server configuration, application deployment, automation

Overview: Ansible is an agentless automation tool that uses YAML playbooks to define tasks, roles, and workflows. It’s particularly strong for configuration management and multi-tier application deployments.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Ansible Playbook - Deploy NGINX Web Server
---
- name: Deploy NGINX Web Server
  hosts: webservers
  become: yes
  
  vars:
    nginx_port: 80
    document_root: /var/www/html
  
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
    
    - name: Install NGINX
      apt:
        name: nginx
        state: present
    
    - name: Copy website files
      copy:
        src: files/index.html
        dest: "{{ document_root }}/index.html"
        owner: www-data
        group: www-data
        mode: '0644'
    
    - name: Configure NGINX
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Reload NGINX
    
    - name: Ensure NGINX is started
      service:
        name: nginx
        state: started
        enabled: yes
  
  handlers:
    - name: Reload NGINX
      service:
        name: nginx
        state: reloaded

Strengths:

  • Simple, readable YAML syntax
  • Agentless (uses SSH)
  • Large module library
  • Strong community support
  • Excellent for configuration management

Weaknesses:

  • Slower for large-scale infrastructure
  • Not ideal for cloud resource provisioning
  • Can become complex with many roles

Best For: Server configuration, application deployment, automation tasks, multi-cloud orchestration


2. Kubernetes

Type: Container Orchestration
Primary Use: Container deployment and management

Overview: Kubernetes uses YAML manifests to define containerized applications, services, deployments, and cluster resources. It’s the industry standard for container orchestration.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Kubernetes Deployment and Service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Strengths:

  • Industry-standard container orchestration
  • Self-healing and auto-scaling
  • Declarative and idempotent
  • Strong ecosystem
  • Cloud-agnostic

Weaknesses:

  • Steep learning curve
  • Complex for simple applications
  • Requires cluster management
  • Verbose YAML files

Best For: Container orchestration, microservices, cloud-native applications


3. Helm

Type: Kubernetes Package Manager
Primary Use: Kubernetes application packaging and templating

Overview: Helm extends Kubernetes with templating capabilities, allowing you to parameterize YAML manifests and package applications as reusable charts.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Helm Chart - values.yaml
replicaCount: 3

image:
  repository: nginx
  tag: "1.21"
  pullPolicy: IfNotPresent

service:
  type: LoadBalancer
  port: 80

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: nginx
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  limits:
    cpu: 500m
    memory: 128Mi
  requests:
    cpu: 250m
    memory: 64Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# Deployment template (templates/deployment.yaml)
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ include "mychart.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "mychart.name" . }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: 80
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

Strengths:

  • Templating and parameterization
  • Package management for Kubernetes
  • Version control for releases
  • Large chart repository
  • Rollback capabilities

Weaknesses:

  • Adds complexity to Kubernetes
  • Template syntax can be confusing
  • Debugging can be difficult
  • Limited to Kubernetes

Best For: Managing complex Kubernetes applications, reusable application templates, versioned deployments


4. Docker Compose

Type: Container Orchestration (Development)
Primary Use: Multi-container application definition

Overview: Docker Compose uses YAML to define multi-container applications, making it easy to spin up development environments and simple production deployments.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# docker-compose.yml - Full Stack Application
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_volume:/static
    depends_on:
      - web
    networks:
      - frontend
    restart: unless-stopped

  web:
    build:
      context: ./app
      dockerfile: Dockerfile
    command: gunicorn myapp.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - ./app:/app
      - static_volume:/app/static
    environment:
      - DEBUG=False
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    networks:
      - frontend
      - backend
    restart: unless-stopped

  db:
    image: postgres:14-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    networks:
      - backend
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped

  celery:
    build:
      context: ./app
      dockerfile: Dockerfile
    command: celery -A myapp worker -l info
    volumes:
      - ./app:/app
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    networks:
      - backend
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_volume:

networks:
  frontend:
  backend:

Strengths:

  • Simple and intuitive
  • Perfect for local development
  • Quick multi-container setup
  • Great documentation
  • Wide adoption

Weaknesses:

  • Not suitable for production at scale
  • Limited orchestration features
  • Single-host deployment
  • No built-in load balancing

Best For: Local development environments, simple multi-container applications, testing


5. AWS CloudFormation

Type: Cloud Infrastructure Provisioning
Primary Use: AWS resource management

Overview: CloudFormation uses YAML (or JSON) templates to provision and manage AWS infrastructure as code. It provides native integration with all AWS services.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# CloudFormation - VPC with EC2 and RDS
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Web Application Stack with VPC, EC2, and RDS'

Parameters:
  Environment:
    Type: String
    Default: production
    AllowedValues:
      - development
      - staging
      - production
  
  InstanceType:
    Type: String
    Default: t3.micro
    AllowedValues:
      - t3.micro
      - t3.small
      - t3.medium

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-vpc
  
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-public-subnet
  
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-private-subnet
  
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-igw
  
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for web server
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-web-sg
  
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: ami-0c55b159cbfafe1f0
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref WebServerSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-web-server
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum update -y
          yum install -y nginx
          systemctl start nginx
          systemctl enable nginx

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub ${Environment}-VPC-ID
  
  WebServerPublicIP:
    Description: Web Server Public IP
    Value: !GetAtt WebServer.PublicIp

Strengths:

  • Native AWS integration
  • Supports all AWS services
  • Drift detection
  • Free to use
  • Stack-based management

Weaknesses:

  • AWS-only (vendor lock-in)
  • Verbose YAML syntax
  • Slower execution
  • Limited error messages
  • Complex intrinsic functions

Best For: AWS-native infrastructure, enterprise AWS deployments, compliance-driven environments


6. Google Cloud Deployment Manager

Type: Cloud Infrastructure Provisioning
Primary Use: Google Cloud Platform resource management

Overview: Deployment Manager uses YAML to define GCP resources, with optional Python or Jinja2 templating for advanced scenarios.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# GCP Deployment Manager - Compute Engine with Load Balancer
resources:
- name: web-instance-template
  type: compute.v1.instanceTemplate
  properties:
    properties:
      machineType: n1-standard-1
      disks:
      - deviceName: boot
        type: PERSISTENT
        boot: true
        autoDelete: true
        initializeParams:
          sourceImage: projects/debian-cloud/global/images/family/debian-11
      networkInterfaces:
      - network: global/networks/default
        accessConfigs:
        - name: External NAT
          type: ONE_TO_ONE_NAT
      metadata:
        items:
        - key: startup-script
          value: |
            #!/bin/bash
            apt-get update
            apt-get install -y nginx
            systemctl start nginx

- name: web-instance-group
  type: compute.v1.instanceGroupManager
  properties:
    baseInstanceName: web-instance
    instanceTemplate: $(ref.web-instance-template.selfLink)
    targetSize: 3
    zone: us-central1-a

- name: web-health-check
  type: compute.v1.httpHealthCheck
  properties:
    port: 80
    requestPath: /
    checkIntervalSec: 5
    timeoutSec: 5
    unhealthyThreshold: 2
    healthyThreshold: 2

- name: web-backend-service
  type: compute.v1.backendService
  properties:
    backends:
    - group: $(ref.web-instance-group.instanceGroup)
      balancingMode: UTILIZATION
      maxUtilization: 0.8
    healthChecks:
    - $(ref.web-health-check.selfLink)
    protocol: HTTP
    port: 80
    timeoutSec: 30

- name: web-url-map
  type: compute.v1.urlMap
  properties:
    defaultService: $(ref.web-backend-service.selfLink)

- name: web-target-proxy
  type: compute.v1.targetHttpProxy
  properties:
    urlMap: $(ref.web-url-map.selfLink)

- name: web-forwarding-rule
  type: compute.v1.globalForwardingRule
  properties:
    target: $(ref.web-target-proxy.selfLink)
    portRange: 80

outputs:
- name: load-balancer-ip
  value: $(ref.web-forwarding-rule.IPAddress)

Strengths:

  • Native GCP integration
  • Python/Jinja2 templating
  • Preview deployments
  • Free to use
  • Template-based reusability

Weaknesses:

  • GCP-only (vendor lock-in)
  • Smaller community
  • Less mature than competitors
  • Limited documentation
  • Complex syntax for advanced use cases

Best For: GCP-native infrastructure, GCP-first organizations


7. GitHub Actions

Type: CI/CD and Automation
Primary Use: Workflow automation, CI/CD pipelines

Overview: GitHub Actions uses YAML to define workflows for continuous integration, deployment, and automation tasks triggered by GitHub events.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# GitHub Actions - CI/CD Pipeline
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch:

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11']
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
      
      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
      
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install pytest pytest-cov
      
      - name: Run tests
        run: |
          pytest --cov=app --cov-report=xml
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml

  lint:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install linting tools
        run: |
          pip install flake8 black isort
      
      - name: Run linters
        run: |
          flake8 app/
          black --check app/
          isort --check-only app/

  build:
    needs: [test, lint]
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Log in to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.DOCKER_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix={{branch}}-
            type=semver,pattern={{version}}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://myapp.example.com
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Configure kubectl
        uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBE_CONFIG }}
      
      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/myapp \
            myapp=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }}
          kubectl rollout status deployment/myapp
      
      - name: Verify deployment
        run: |
          kubectl get pods -l app=myapp

Strengths:

  • Native GitHub integration
  • Free for public repositories
  • Matrix builds
  • Large action marketplace
  • Excellent documentation

Weaknesses:

  • GitHub-specific
  • Limited debugging
  • Can be complex for advanced workflows
  • Minutes consumption for private repos

Best For: GitHub projects, CI/CD automation, repository workflows


8. GitLab CI/CD

Type: CI/CD and Automation
Primary Use: Pipeline automation, continuous deployment

Overview: GitLab CI/CD uses .gitlab-ci.yml to define pipelines with stages, jobs, and deployment strategies.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# .gitlab-ci.yml - Complete CI/CD Pipeline
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

stages:
  - test
  - build
  - deploy

.test-template: &test-template
  stage: test
  image: python:3.11
  before_script:
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'

test:unit:
  <<: *test-template
  script:
    - pytest tests/unit/ --cov=app --cov-report=term --cov-report=xml
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

test:integration:
  <<: *test-template
  services:
    - postgres:14
    - redis:7
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: testuser
    POSTGRES_PASSWORD: testpass
  script:
    - pytest tests/integration/

lint:
  stage: test
  image: python:3.11
  script:
    - pip install flake8 black isort
    - flake8 app/
    - black --check app/
    - isort --check-only app/

security:
  stage: test
  image: python:3.11
  script:
    - pip install bandit safety
    - bandit -r app/
    - safety check

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  only:
    - main
    - develop

deploy:staging:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache curl
    - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
    - chmod +x kubectl
    - mv kubectl /usr/local/bin/
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/myapp myapp=$IMAGE_TAG
    - kubectl rollout status deployment/myapp
  environment:
    name: staging
    url: https://staging.myapp.example.com
  only:
    - develop

deploy:production:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache curl
    - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
    - chmod +x kubectl
    - mv kubectl /usr/local/bin/
  script:
    - kubectl config use-context production
    - kubectl set image deployment/myapp myapp=$IMAGE_TAG
    - kubectl rollout status deployment/myapp
  environment:
    name: production
    url: https://myapp.example.com
  when: manual
  only:
    - main

Strengths:

  • Integrated with GitLab
  • Free CI/CD minutes
  • Built-in container registry
  • Auto DevOps
  • Excellent features

Weaknesses:

  • GitLab-specific
  • Self-hosted option can be complex
  • Learning curve for advanced features

Best For: GitLab projects, integrated DevOps workflows


9. CircleCI

Type: CI/CD Platform
Primary Use: Continuous integration and deployment

Overview: CircleCI uses .circleci/config.yml to define build, test, and deployment workflows with powerful caching and parallelization features.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# .circleci/config.yml - Advanced CI/CD Pipeline
version: 2.1

orbs:
  node: circleci/node@5.0
  docker: circleci/docker@2.1
  kubernetes: circleci/kubernetes@1.3

executors:
  python-executor:
    docker:
      - image: cimg/python:3.11
    resource_class: medium

jobs:
  test:
    executor: python-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "requirements.txt" }}
            - v1-dependencies-
      - run:
          name: Install dependencies
          command: |
            python -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
      - save_cache:
          paths:
            - ./venv
          key: v1-dependencies-{{ checksum "requirements.txt" }}
      - run:
          name: Run tests
          command: |
            . venv/bin/activate
            pytest --junitxml=test-results/junit.xml --cov=app --cov-report=html
      - store_test_results:
          path: test-results
      - store_artifacts:
          path: htmlcov

  lint:
    executor: python-executor
    steps:
      - checkout
      - run:
          name: Install linting tools
          command: pip install flake8 black isort mypy
      - run:
          name: Run linters
          command: |
            flake8 app/
            black --check app/
            isort --check-only app/
            mypy app/

  build:
    executor: docker/docker
    steps:
      - checkout
      - setup_remote_docker:
          version: 20.10.14
      - docker/check
      - docker/build:
          image: myorg/myapp
          tag: ${CIRCLE_SHA1}
      - docker/push:
          image: myorg/myapp
          tag: ${CIRCLE_SHA1}

  deploy:
    executor: kubernetes/default
    steps:
      - checkout
      - kubernetes/install-kubectl
      - run:
          name: Deploy to Kubernetes
          command: |
            kubectl set image deployment/myapp \
              myapp=myorg/myapp:${CIRCLE_SHA1}
            kubectl rollout status deployment/myapp

workflows:
  version: 2
  build-and-deploy:
    jobs:
      - test
      - lint
      - build:
          requires:
            - test
            - lint
          filters:
            branches:
              only: main
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: main

Strengths:

  • Fast execution
  • Advanced caching
  • Matrix builds
  • SSH debugging
  • Great performance

Weaknesses:

  • Cost for private projects
  • Learning curve
  • Limited free tier

Best For: Performance-critical CI/CD, enterprise projects


YAML Best Practices for IaC

1. Use Consistent Indentation

Always use 2 spaces for indentation (never tabs):

1
2
3
4
5
6
7
8
9
10
11
12
13
# Good
services:
  web:
    image: nginx
    ports:
      - "80:80"

# Bad (inconsistent spacing)
services:
   web:
      image: nginx
   ports:
       - "80:80"

2. Use Comments Liberally

Document complex configurations and explain non-obvious decisions:

1
2
3
4
5
6
7
8
9
10
11
12
# Production database configuration
# Uses read replicas for improved performance
database:
  primary:
    host: db-primary.example.com
    port: 5432
  # Read-only replicas for SELECT queries
  replicas:
    - host: db-replica-1.example.com
      port: 5432
    - host: db-replica-2.example.com
      port: 5432

3. Use Anchors and Aliases for Reusability

Avoid repetition with YAML anchors (&) and aliases (*):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Define common configuration
.common-config: &common
  memory: 512Mi
  cpu: 250m
  restart_policy: always

services:
  web:
    <<: *common
    image: nginx
  
  api:
    <<: *common
    image: myapi

4. Validate YAML Syntax

Use linters and validators before deployment:

1
2
3
4
5
6
# Use yamllint
yamllint config.yml

# Use online validators
# - https://www.yamllint.com/
# - https://jsonformatter.org/yaml-validator

5. Use Version Control

Always version control your YAML files:

1
2
3
git add infrastructure.yml
git commit -m "Add production database configuration"
git push origin main

6. Separate Environments

Use different files for different environments:

1
2
3
4
5
infrastructure/
  ├── base.yml          # Common configuration
  ├── development.yml   # Dev overrides
  ├── staging.yml       # Staging overrides
  └── production.yml    # Production configuration

7. Use Variables and Parameters

Parameterize values that change between environments:

1
2
3
4
5
6
7
# Docker Compose with environment variables
services:
  web:
    image: ${IMAGE_NAME:-nginx:latest}
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - API_KEY=${API_KEY}

8. Keep Files Focused and Modular

Split large configurations into smaller, focused files:

1
2
3
4
5
6
# main.yml
includes:
  - network.yml
  - compute.yml
  - storage.yml
  - security.yml

9. Use Multi-line Strings Properly

Choose the right style for multi-line content:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Literal style (preserves newlines and indentation)
script: |
  #!/bin/bash
  echo "Starting deployment"
  kubectl apply -f deployment.yml

# Folded style (folds newlines to spaces)
description: >
  This is a long description that will be
  folded into a single line with spaces.

# Plain style (for simple strings)
name: my-application

10. Document Required Variables

Use comments to document required environment variables or parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Required environment variables:
# - DATABASE_URL: PostgreSQL connection string
# - REDIS_URL: Redis connection string
# - SECRET_KEY: Application secret key (min 32 characters)
# - AWS_ACCESS_KEY_ID: AWS access key
# - AWS_SECRET_ACCESS_KEY: AWS secret key

services:
  app:
    environment:
      DATABASE_URL: ${DATABASE_URL}
      REDIS_URL: ${REDIS_URL}
      SECRET_KEY: ${SECRET_KEY}

Comparison Table

ToolTypeLearning CurveCloudBest Use Case
AnsibleConfig ManagementLowMultiServer configuration, automation
KubernetesOrchestrationHighMultiContainer orchestration
HelmPackage ManagerModerateMultiKubernetes app packaging
Docker ComposeContainer ToolLowMultiLocal development, simple apps
CloudFormationCloud IaCModerateAWSAWS infrastructure
GCP Deployment ManagerCloud IaCModerateGCPGCP infrastructure
GitHub ActionsCI/CDLowMultiGitHub automation
GitLab CI/CDCI/CDModerateMultiGitLab projects
CircleCICI/CDModerateMultiHigh-performance CI/CD

Choosing the Right YAML IaC Tool

For Configuration Management

Choose Ansible when you need to:

  • Configure servers and applications
  • Automate repetitive tasks
  • Work across multiple cloud providers
  • Use simple, readable syntax

For Container Orchestration

Choose Kubernetes when you need:

  • Production-grade container orchestration
  • Auto-scaling and self-healing
  • Service discovery and load balancing
  • Cloud-native applications

Choose Docker Compose when you need:

  • Local development environments
  • Simple multi-container applications
  • Quick prototyping
  • Single-host deployments

For Cloud Infrastructure

Choose CloudFormation for AWS-only infrastructure with:

  • Deep AWS service integration
  • Compliance requirements
  • Drift detection needs
  • Stack-based management

Choose GCP Deployment Manager for GCP-first organizations

For CI/CD Pipelines

Choose GitHub Actions for GitHub projects with native integration

Choose GitLab CI/CD for comprehensive DevOps platform

Choose CircleCI for performance-critical pipelines


Common Pitfalls and Solutions

Pitfall 1: Indentation Errors

Problem: YAML is indentation-sensitive

Solution: Use a YAML-aware editor with syntax highlighting and validation

1
2
3
4
5
6
7
8
9
# Wrong (mixed spaces and tabs)
services:
	web:
  image: nginx

# Correct (consistent 2-space indentation)
services:
  web:
    image: nginx

Pitfall 2: Unquoted Special Characters

Problem: Special characters can break YAML parsing

Solution: Quote strings with special characters

1
2
3
4
5
# Wrong
password: p@ssw0rd!123

# Correct
password: "p@ssw0rd!123"

Pitfall 3: Implicit Type Conversion

Problem: YAML auto-converts values to types

Solution: Quote values to preserve them as strings

1
2
3
4
5
6
7
8
# This becomes boolean true, not string
enabled: yes

# This stays as string "yes"
enabled: "yes"

# Version numbers should be quoted
version: "3.8"  # Not 3.8 (float)

Pitfall 4: Duplica Keys

Problem: YAML allows duplicate keys (last wins)

Solution: Use linters to catch duplicates

1
2
3
4
5
6
7
8
9
10
# Wrong (duplicate key)
database:
  host: localhost
  port: 5432
  host: db.example.com  # This overwrites the first host

# Correct
database:
  host: db.example.com
  port: 5432

Pitfall 5: Large Files

Problem: YAML files become unmanageable when large

Solution: Split into multiple files and use includes/imports

1
2
3
4
5
# main.yml
include:
  - services/web.yml
  - services/database.yml
  - services/cache.yml

Security Best Practices

1. Never Commit Secrets

Use secret management tools instead of hardcoding secrets:

1
2
3
4
5
6
7
8
9
# Wrong - secrets in plain text
database:
  password: "MyP@ssw0rd123"
  api_key: "sk_live_abc123def456"

# Correct - use secret management
database:
  password: ${DATABASE_PASSWORD}  # From environment
  api_key: ${API_KEY}              # From secret manager

2. Use Secret Management Tools

  • Kubernetes Secrets for Kubernetes
  • AWS Secrets Manager for AWS
  • HashiCorp Vault for multi-cloud
  • Azure Key Vault for Azure
  • GCP Secret Manager for GCP

3. Encrypt Sensitive Files

Use tools like git-crypt or ansible-vault:

1
2
3
4
5
# Encrypt with ansible-vault
ansible-vault encrypt secrets.yml

# Use in playbook
ansible-playbook --ask-vault-pass playbook.yml

4. Implement RBAC

Define proper access controls:

1
2
3
4
5
6
7
8
9
# Kubernetes RBAC
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]

5. Scan for Vulnerabilities

Use security scanning tools:

1
2
3
4
5
6
7
8
# Scan Kubernetes manifests
kubesec scan deployment.yml

# Scan Docker Compose files
docker-compose config --quiet && echo "Valid"

# Use Checkov for IaC scanning
checkov -f cloudformation.yml

Conclusion

YAML has become the lingua franca of Infrastructure as Code, offering a human-readable format that works across countless tools and platforms. While each tool has its strengths and use cases, they all benefit from YAML’s simplicity and declarative nature.

Key Takeaways:

  • YAML’s readability makes it ideal for IaC
  • Choose tools based on your specific use case (config management vs. orchestration vs. CI/CD)
  • Follow best practices for indentation, validation, and organization
  • Never commit secrets - use proper secret management
  • Use linters and validators to catch errors early
  • Split large files into modular components
  • Document your configurations with comments

Whether you’re managing servers with Ansible, orchestrating containers with Kubernetes, or building CI/CD pipelines with GitHub Actions, mastering YAML is essential for modern DevOps practices.

This post is licensed under CC BY 4.0 by the author.