CI/CD Pipelines 101: Building Your First Automated Deployment
Every team eventually hits the same wall: manually testing and deploying code works fine with one developer and a handful of commits a week, but it falls apart as the team and codebase grow. CI/CD is the standard answer, and it's simpler than most explanations make it seem.
CI and CD are two different things
Continuous Integration (CI) is the practice of automatically building and testing every code change as soon as it's pushed, rather than waiting until a big merge. The goal is to catch integration problems and bugs within minutes of them being introduced, not weeks later.
Continuous Delivery means every change that passes CI is automatically prepared for release, packaged, versioned, and ready to ship, but a human still clicks the button to actually release it.
Continuous Deployment goes one step further: passing changes deploy to production automatically, with no manual gate at all.
Most teams land somewhere between Delivery and Deployment: automatic deploys to staging, manual approval for production.
Anatomy of a pipeline
A pipeline is just a series of automated stages that run in sequence (or in parallel where possible) whenever code changes. A typical one looks like:
- Trigger: a push or pull request against a branch.
- Install & build: install dependencies, compile/build the application.
- Test: run unit tests, and often integration tests.
- Static analysis: linting, type checking, security scanning.
- Package: build a Docker image or deployable artifact, tag it (often with the Git commit SHA).
- Deploy: push the artifact to staging, and to production after any required approval.
If any stage fails, the pipeline stops and the team is notified. Bad code never silently reaches production.
A minimal CI/CD pipeline example with GitHub Actions
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm test
This runs on every push and pull request against main: checks out the code, installs Node.js, installs dependencies with a clean, reproducible npm ci, builds the project, and runs the test suite. Extending it to deploy is just adding another job that runs after this one succeeds, typically gated to only run on the main branch.
Deployment strategies worth knowing
Automating deployment raises a new question: how do you release changes without downtime or risk? A few strategies come up constantly, including in interviews:
- Rolling deployment: replace old instances with new ones gradually, a few at a time. Simple, but a bad release can affect part of your traffic while it's rolling out.
- Blue-green deployment: run two identical production environments ("blue" and "green"). Deploy the new version to the idle one, test it, then switch traffic over instantly. Rolling back is just switching traffic back.
- Canary deployment: release the new version to a small percentage of traffic first, watch metrics, then gradually increase the percentage if things look healthy. This limits the blast radius of a bad release.
Trunk-based development and feature flags
Long-lived feature branches tend to produce painful merges. Trunk-based development, everyone integrating small changes into the main branch frequently, plays well with CI/CD because it keeps every change small and easy to validate quickly.
Incomplete features being merged to main doesn't have to mean they're live for users: feature flags let you merge code behind a toggle and turn it on for specific users or percentages of traffic later, decoupling deployment from release entirely.
GitOps: pushing this idea further
GitOps takes the CI/CD philosophy and applies it to infrastructure itself: your Git repository becomes the single source of truth for what should be running, and an operator (like ArgoCD or Flux) continuously reconciles the actual cluster state to match what's declared in Git. Instead of running deploy commands, you open a pull request.
Getting started practically
If your project has no pipeline today, start small: a single CI job that installs dependencies and runs tests on every pull request already prevents a large share of avoidable bugs. Add deployment automation once that foundation is solid rather than trying to build the whole pipeline at once.
Ready to check your understanding? Try the CI/CD quiz on OpsQuiz.