Multi-Stage Pipelines
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
Mastering Multi-Stage YAML Pipelines
Introduction: The Architecture of Modern Delivery
In the early days of automation, build processes were often simple, linear sequences: compile code, run tests, and maybe move a file to a server. As software systems grew in complexity, these linear scripts became brittle and difficult to manage. Today, we rely on Multi-Stage Pipelines to orchestrate the entire lifecycle of an application, from the moment code is committed to a repository until it is running in a production environment. A multi-stage pipeline is not just a build script; it is a declarative definition of your software delivery process, structured into distinct, logical phases that represent the progression of your application through different environments.
Understanding multi-stage pipelines is crucial because they provide the visibility and control necessary for professional-grade software delivery. When you break a pipeline into stages like "Build," "Test," "Staging," and "Production," you gain the ability to enforce quality gates, manage environment-specific configurations, and track the progress of every change. This structure allows teams to adopt advanced deployment strategies like canary releases or blue-green deployments with confidence, ensuring that if something goes wrong, you have clear insight into exactly where the process failed. This lesson will guide you through the conceptual framework, the technical implementation, and the best practices for building durable, maintainable multi-stage pipelines.
Understanding the Structure of a Multi-Stage Pipeline
At its core, a multi-stage pipeline is defined by a hierarchy of components: Stages, Jobs, and Steps. While a simple pipeline might contain only a single job, a multi-stage pipeline groups these jobs into logical containers called "Stages." A stage acts as a boundary; it is a logical grouping of work that must complete before the next stage can begin. This allows you to treat your pipeline as a sequence of checkpoints where you can perform manual approvals, run automated security scans, or wait for external conditions to be met.
Hierarchy Breakdown
- Stages: The top-level containers. A pipeline is composed of one or more stages. Stages are typically sequential, meaning Stage B won't start until Stage A finishes, though they can be configured to run in parallel if independent.
- Jobs: Within each stage, you define one or more jobs. A job is a unit of work that runs on a specific agent (a virtual machine or container). If you have multiple jobs within a stage, they run in parallel by default, allowing you to optimize build times.
- Steps: The individual tasks inside a job. These are the shell scripts, command-line instructions, or predefined tasks (like "copy files" or "run tests") that perform the actual heavy lifting.
Callout: Stages vs. Jobs It is a common point of confusion to distinguish between stages and jobs. Think of a Stage as a milestone—a "gate" that your code passes through (e.g., "Build complete," "QA approved," "Deployed to Prod"). Think of a Job as the actual work performed on a specific machine. You might have a "Build" stage with one job for compiling the app, or a "Test" stage with three parallel jobs running different types of test suites (unit, integration, and UI).
Implementing Your First Multi-Stage Pipeline
To implement a multi-stage pipeline, you use the YAML format. YAML is highly readable, allowing you to version control your pipeline definition alongside your application code. This practice, often called "Pipeline as Code," ensures that your deployment logic is audited, peer-reviewed, and consistent across all branches.
Example: A Standard Three-Stage Pipeline
Consider a common scenario: you need to build your application, deploy it to a staging environment, and finally deploy it to production.
trigger:
- main
stages:
- stage: Build
jobs:
- job: CompileAndPackage
pool:
vmImage: 'ubuntu-latest'
steps:
- script: dotnet build --configuration Release
- script: dotnet publish --output $(Build.ArtifactStagingDirectory)
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: DeployToStaging
dependsOn: Build
jobs:
- job: Deploy
pool:
vmImage: 'ubuntu-latest'
steps:
- download: current
artifact: drop
- script: ./deploy_script.sh --env staging
- stage: DeployToProduction
dependsOn: DeployToStaging
jobs:
- deployment: Production
environment: 'Production-Env'
strategy:
runOnce:
deploy:
steps:
- script: ./deploy_script.sh --env production
In the example above, the dependsOn keyword is critical. It explicitly tells the pipeline runner that DeployToStaging cannot start until the Build stage finishes successfully. This creates a dependency graph that the pipeline engine uses to schedule your work.
Note: The
deploymentjob type is a special type of job in many CI/CD systems. It provides built-in support for tracking deployment history, managing environment-specific variables, and integrating with manual approval gates. Always usedeploymentjobs for tasks that modify your production or staging environments.
Advanced Configuration: Parallelism and Dependencies
One of the primary benefits of the multi-stage approach is the ability to optimize for speed. If you have a large test suite, running every test in a single job could take an hour. By splitting these into separate jobs within a single stage, you can distribute the load across multiple agents, reducing your total pipeline time significantly.
Parallel Job Execution
You can define multiple jobs inside a single stage, and by default, they will run concurrently.
stages:
- stage: RunTests
jobs:
- job: UnitTest
steps:
- script: npm run test:unit
- job: IntegrationTest
steps:
- script: npm run test:integration
- job: Linting
steps:
- script: npm run lint
In this case, the UnitTest, IntegrationTest, and Linting jobs will all start at the same time. The RunTests stage will only be considered "successful" once all three jobs have finished without errors. If one fails, the entire stage fails, preventing the deployment stages from ever starting.
Managing Environments and Approvals
In a professional environment, you should never deploy to production automatically without a safety net. Multi-stage pipelines allow you to define "Environments," which act as a target for your deployment jobs. You can associate these environments with security policies, such as requiring a specific user or group to manually approve a deployment before it proceeds.
Step-by-Step: Adding an Approval Gate
- Navigate to the "Environments" section in your CI/CD platform.
- Create a new environment named "Production".
- Click on the "Approvals and Checks" tab within that environment.
- Add a "Checks" requirement (e.g., "Approvals").
- Select the users or teams authorized to approve deployments.
- Update your YAML to target this environment:
- stage: Production
jobs:
- deployment: DeployProd
environment: 'Production' # This triggers the checks defined in the UI
strategy:
runOnce:
deploy:
steps:
- script: ./deploy.sh
When the pipeline reaches the Production stage, it will pause. The status will show "Waiting for approval." The designated approvers will receive a notification, and they must manually sign off on the deployment in the web interface. This is a fundamental practice for maintaining control over production releases.
Callout: Environment-Level Security Environments are not just labels. They are security boundaries. By using environments, you can ensure that only authorized service accounts have the credentials to modify production resources. This separation of concerns—where the pipeline code defines what happens, and the environment settings define who is allowed to trigger it—is a hallmark of secure DevOps practices.
Best Practices for Pipeline Maintenance
Maintaining pipelines can become a full-time job if you don't follow a few core principles. As your organization scales, the complexity of your pipelines will naturally increase, making discipline essential.
1. Keep Logic Out of the YAML
Do not write complex shell scripts directly inside your YAML file. If you have a 50-line bash script, move it into a separate file within your repository (e.g., scripts/deploy.sh) and have the pipeline execute that file. This makes your scripts testable locally on your machine, outside of the pipeline environment.
2. Use Templates for Reusability
If you have five different microservices that all follow the same build-and-deploy process, don't copy-paste the YAML for each one. Use templates. A template allows you to define a standard "Build" or "Deploy" procedure in one file and reference it in multiple pipelines.
# templates/build-template.yml
parameters:
- name: buildConfiguration
type: string
default: 'Release'
steps:
- script: dotnet build --configuration ${{ parameters.buildConfiguration }}
You can then include this in your main pipeline using the template keyword. This ensures that when you need to update your build process, you only have to change it in one location.
3. Fail Fast
Configure your pipeline to stop as soon as a failure occurs. If your unit tests fail, there is no reason to run the integration tests or attempt a deployment. The structure of stages naturally supports this, as a failure in one stage prevents subsequent stages from starting.
4. Version Your Pipelines
Just as you version your application code, you should treat your pipeline definitions as versioned assets. Avoid making changes to pipelines directly in the web UI. Always perform changes in a branch, submit a pull request, and review the changes before merging them into the main branch.
Common Pitfalls and How to Avoid Them
Even experienced engineers run into issues when managing multi-stage pipelines. Recognizing these early can save you hours of debugging.
The "Environment Variable" Trap
A common mistake is assuming that variables defined in one job are automatically available in another. By default, they are not. If you need to pass a value from a build job to a deployment job, you must use "Output Variables."
# Job A
- script: echo "##vso[task.setvariable variable=myOutput;isOutput=true]someValue"
name: setVarStep
# Job B
- script: echo $(dependencies.JobA.outputs['setVarStep.myOutput'])
This syntax can be tricky. Always check your platform's documentation regarding variable scope and cross-job communication, as it is one of the most frequent sources of "pipeline-only" bugs.
Hardcoding Credentials
Never put secrets, API keys, or passwords directly into your YAML files. Even if your repository is private, this is a major security risk. Use the built-in secret management features provided by your CI/CD platform (such as "Variable Groups" or "Secret Files"). These secrets are encrypted and masked in the logs, ensuring that your sensitive information remains secure.
Ignoring Agent Capacity
If you have 50 jobs running in parallel, but your CI/CD platform only provides 5 parallel agents, 45 of your jobs will sit in a "Pending" state. This creates artificial latency. Always monitor your agent usage and ensure that your concurrency settings match your available infrastructure.
Comparison Table: Linear vs. Multi-Stage Pipelines
| Feature | Linear Pipeline | Multi-Stage Pipeline |
|---|---|---|
| Structure | Single sequence of steps | Grouped into logical stages |
| Visibility | Low (hard to see progress) | High (clear status per stage) |
| Approvals | None or limited | Native support (manual/automated) |
| Reusability | Difficult to modularize | Easy via templates |
| Error Handling | Stops entire flow | Isolates failures to a stage |
| Parallelism | Limited | High (parallel jobs/stages) |
Frequently Asked Questions
Q: Can a stage be skipped?
A: Yes. You can use "conditions" on a stage to determine whether it should run. For example, you might want to skip the "Deployment" stage if the build was triggered by a pull request, only running it on merges to the main branch.
Q: How do I handle large artifacts?
A: Pipelines are not designed to be storage servers. If you are building large assets (like container images or massive binaries), push them to a dedicated registry (like Docker Hub, Azure Container Registry, or JFrog Artifactory) during the build stage. Your deployment stage should then pull the artifact from that registry, rather than trying to pass gigabytes of data through the pipeline's internal storage.
Q: What happens if a job times out?
A: Most CI/CD systems have a default timeout (often 60 minutes). If your job exceeds this, it will be terminated. You can usually configure this in the job settings, but if you find yourself needing to increase the timeout significantly, it is often a sign that the job is doing too much and should be broken down into smaller, faster tasks.
Q: Can I trigger one pipeline from another?
A: Yes, this is known as "Pipeline Resources." You can configure Pipeline B to start automatically as soon as Pipeline A completes successfully. This is useful for large, decoupled systems where different teams own different components of the architecture.
Best Practices Checklist for Production Pipelines
To ensure your pipelines are production-ready, verify them against this checklist before deploying to your main branch:
- Does every stage have a clear purpose? (Build, Test, Deploy).
- Are secrets masked? (Check logs to ensure no passwords appear).
- Is there a manual gate for production? (Never deploy to production without a human check).
- Are artifacts published correctly? (Use the built-in artifact storage).
- Is the pipeline idempotent? (Can you run the same stage twice without breaking things?).
- Are dependencies explicit? (Use
dependsOnto control flow). - Is the code in the repository? (No manual changes in the web UI).
Summary: Key Takeaways
- Logical Separation: Use stages to create clear boundaries in your delivery process. This makes the pipeline easier to read and debug.
- Pipeline as Code: Keep your pipeline definition in your repository. This ensures that your deployment process is versioned and reviewable just like your application code.
- Safety Through Environments: Utilize environment-based deployment jobs to enforce security policies and manual approval gates for production releases.
- Efficiency via Parallelism: Leverage concurrent jobs within stages to reduce feedback loops, ensuring developers get test results as quickly as possible.
- Modularity with Templates: Use templates to share common logic across multiple pipelines, reducing maintenance overhead and ensuring consistency across your organization.
- Security First: Never hardcode secrets. Use your platform's built-in secret management and ensure all sensitive values are masked in build logs.
- Fail Fast, Recover Faster: Design your pipeline to stop immediately upon failure and ensure that your deployment processes are repeatable and predictable.
By mastering the multi-stage pipeline, you transition from being a developer who writes code to an engineer who builds systems. You are no longer just concerned with what the code does, but how it reaches the user, how it is tested, and how it is protected. This systematic approach is the foundation of high-performing teams, allowing you to ship features faster and with significantly less risk. Take the time to modularize your pipelines, enforce your gates, and treat your configuration with the same care as your production application.
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons