YAML is widely used in CI/CD (Continuous Integration/Continuous Deployment) pipelines, especially on platforms like GitHub Actions, GitLab CI, and CircleCI. Understanding YAML's application in CI/CD is crucial for DevOps engineers.
Role of YAML in CI/CD
1. Define Pipeline Configuration
YAML files define all steps, trigger conditions, and environment configurations of CI/CD pipelines.
2. Declarative Configuration
YAML allows describing the entire build and deployment process in a declarative manner.
3. Version Control
YAML configuration files can be version controlled and reviewed like code.
YAML Configurations for Common CI/CD Platforms
1. GitHub Actions
GitHub Actions uses YAML files in the .github/workflows/ directory to define workflows.
yaml# .github/workflows/ci.yml name: CI Pipeline on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 0 * * *' # Run daily at midnight env: NODE_VERSION: '18.x' DOCKER_REGISTRY: ghcr.io jobs: test: name: Run Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run linter run: npm run lint - name: Run tests run: npm test - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info build: name: Build Docker Image runs-on: ubuntu-latest needs: test outputs: image-tag: ${{ steps.meta.outputs.tags }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }} tags: | type=ref,event=branch type=sha,prefix= - name: Build and push uses: docker/build-push-action@v5 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: name: Deploy to Production runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Deploy to Kubernetes uses: azure/k8s-deploy@v4 with: manifests: | k8s/deployment.yaml k8s/service.yaml images: | ${{ needs.build.outputs.image-tag }} kubectl-version: 'latest'
2. GitLab CI
GitLab CI uses the .gitlab-ci.yml file in the project root.
yaml# .gitlab-ci.yml stages: - test - build - deploy variables: NODE_VERSION: "18" DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA DOCKER_TLS_CERTDIR: "/certs" cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ before_script: - npm ci test: stage: test image: node:${NODE_VERSION} script: - npm run lint - npm test coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml paths: - coverage/ expire_in: 1 week build: stage: build image: docker:24 services: - docker:24-dind variables: DOCKER_DRIVER: overlay2 before_script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: - docker build -t $DOCKER_IMAGE . - docker push $DOCKER_IMAGE only: - main - develop deploy:staging: stage: deploy image: bitnami/kubectl:latest script: - kubectl config use-context $KUBE_CONTEXT_STAGING - kubectl set image deployment/app app=$DOCKER_IMAGE -n staging - kubectl rollout status deployment/app -n staging environment: name: staging url: https://staging.example.com only: - develop deploy:production: stage: deploy image: bitnami/kubectl:latest script: - kubectl config use-context $KUBE_CONTEXT_PRODUCTION - kubectl set image deployment/app app=$DOCKER_IMAGE -n production - kubectl rollout status deployment/app -n production environment: name: production url: https://example.com when: manual only: - main
3. CircleCI
CircleCI uses the .circleci/config.yml file in the project root.
yaml# .circleci/config.yml version: 2.1 orbs: node: circleci/node@5.1.0 docker: circleci/docker@2.4.0 executors: node-executor: docker: - image: cimg/node:18.19 working_directory: ~/project jobs: test: executor: node-executor steps: - checkout - node/install-packages - run: name: Run linter command: npm run lint - run: name: Run tests command: npm test - run: name: Generate coverage report command: npm run test:coverage - store_test_results: path: test-results - store_artifacts: path: coverage build: executor: docker/docker steps: - checkout - setup_remote_docker - docker/check - docker/build: image: myapp tag: $CIRCLE_SHA1 - docker/push: image: myapp tag: $CIRCLE_SHA1 deploy: executor: node-executor steps: - checkout - run: name: Deploy to production command: | kubectl set image deployment/app \ app=myapp:$CIRCLE_SHA1 \ -n production workflows: version: 2 test-build-deploy: jobs: - test - build: requires: - test filters: branches: only: - main - develop - deploy: requires: - build filters: branches: only: main
Advanced YAML Features in CI/CD
1. Conditional Execution
yaml# GitHub Actions conditional execution deploy: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Deploy run: echo "Deploying to production"
2. Matrix Builds
yaml# GitHub Actions matrix builds test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x, 20.x] os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test
3. Cache Dependencies
yaml# GitHub Actions caching - name: Cache node modules uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-
4. Parallel Execution
yaml# GitLab CI parallel execution test: parallel: 4 script: - npm test -- --shard $CI_NODE_INDEX/$CI_NODE_TOTAL
5. Environment Variables and Secrets
yaml# GitHub Actions environment variables env: DATABASE_URL: ${{ secrets.DATABASE_URL }} API_KEY: ${{ secrets.API_KEY }} steps: - name: Deploy env: ENVIRONMENT: production run: | echo $DATABASE_URL echo $API_KEY
6. Workflow Reuse
yaml# Reusable workflow # .github/workflows/reusable-deploy.yml on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_KEY: required: true jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - name: Deploy run: | echo "Deploying to ${{ inputs.environment }}" echo ${{ secrets.DEPLOY_KEY }}
yaml# Call reusable workflow # .github/workflows/ci.yml jobs: deploy-staging: uses: ./.github/workflows/reusable-deploy.yml with: environment: staging secrets: DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }} deploy-production: uses: ./.github/workflows/reusable-deploy.yml with: environment: production secrets: DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}
Best Practices
1. Modular Configuration
yaml# Use YAML anchors and aliases to reuse configuration .defaults: &defaults runs-on: ubuntu-latest timeout-minutes: 30 job1: <<: *defaults steps: - run: echo "Job 1" job2: <<: *defaults steps: - run: echo "Job 2"
2. Use Environment Variables
yamlenv: NODE_ENV: production LOG_LEVEL: info jobs: build: env: BUILD_ENV: ${{ github.ref }} steps: - run: echo $NODE_ENV
3. Error Handling
yamlsteps: - name: Run tests id: test continue-on-error: true run: npm test - name: Upload test results if: always() && steps.test.outcome == 'failure' uses: actions/upload-artifact@v3 with: name: test-results path: test-results/
4. Resource Optimization
yaml# Use concurrency limits concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # Use timeouts jobs: test: timeout-minutes: 30 steps: - run: npm test
Common Issues and Solutions
1. YAML Syntax Errors
yaml# ❌ Error: Inconsistent indentation jobs: test: runs-on: ubuntu-latest steps: - run: echo "test" # ✅ Correct: Consistent indentation jobs: test: runs-on: ubuntu-latest steps: - run: echo "test"
2. Undefined Environment Variables
yaml# Use default values env: DATABASE_URL: ${{ secrets.DATABASE_URL || 'sqlite://:memory:' }}
3. Dependency Cache Invalidation
yaml# Use versioned cache keys - uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.nvmrc') }}
Tools and Resources
- GitHub Actions Linter: https://actionlint.github.io/
- GitLab CI Linter: Built into GitLab UI
- CircleCI Config Validator: https://circleci.com/docs/2.0/configuration-reference/
- CI/CD YAML Editor Plugins: VS Code extensions
Mastering YAML's application in CI/CD can significantly improve development efficiency and deployment quality.