CI/CD Pipeline
Hosted build and test service
Design a hosted service that runs a repository's build, test and deploy jobs on every push, in isolated machines, and streams their logs live.
Overview
A CI/CD service runs the checks a team wants on every change. A developer pushes a commit; the service reads a pipeline file from the repository, splits it into jobs (install, lint, test, build, deploy), runs each job on a machine called a runner, and reports pass or fail back on the commit. Continuous integration is the build-and-test part; continuous delivery or deployment adds the jobs that ship the result.
It is a scheduling problem with a security problem inside it. Jobs have an order, so the service must run them as a graph and start each one as soon as what it needs has finished. Demand comes in bursts, so the pool of runners must grow and shrink. And a job runs code the service did not write, often from strangers who opened a pull request, so every job gets a clean machine and only trusted runs see secrets.
Questions to ask first
- Who are the users, and how many runs? — A hosted service for many organizations; 2 million pipeline runs a day
- What starts a run? — Pushes, pull requests, tags, schedules and manual starts
- How are jobs defined? — A YAML file in the repository; each job lists the jobs it needs
- Do we run untrusted code? — Yes: pull requests from forks of public repositories
- Do we deploy too? — Yes, as ordinary jobs; the rollout strategy is a separate system
- How fresh must logs be? — Visible within a couple of seconds while the job runs
What you will be able to do
- Turn a pipeline file into a job graph and schedule it on the critical path.
- Design a job queue that survives runner crashes, using leases.
- Size and autoscale a runner pool from the traffic numbers.
- Decide which runs may see secrets and write caches, and why.
- Stream live logs without putting them through the database.
Key concepts
- Webhook triggers
- Job graph (DAG)
- Critical path
- Job queue with leases
- Ephemeral runners
- Autoscaling
- Content-keyed caches
- Artifacts
- Secret masking
- Concurrency groups
Functional requirements
- Start a run on push, pull request, tag, schedule or a manual request
- Read the pipeline file at that commit and build a job graph from it
- Run each job on a fresh, isolated runner once the jobs it needs succeed
- Restore and save dependency caches; upload and download artifacts
- Give secrets to trusted runs only, and mask them in logs
- Stream each job's log live, and keep it after the job ends
- Cancel a run; let only one deploy per environment run at a time
Non-functional requirements
- A job starts within 30 seconds of becoming ready at p95 (a target we chose)
- Log lines reach a watching browser within 2 seconds (a target we chose)
- No job can read another job's files, memory or secrets
- A runner crash never loses a job silently: it is retried or marked failed
- The service keeps accepting triggers when runners are short; jobs wait in the queue
Interview questions
- How do you turn a pipeline file into running jobs?
- A runner dies halfway through a job. What happens?
- How do you run code from strangers safely?
- How do you make builds fast?
- How do you stream logs to thousands of watching developers?
- How do you size the runner pool?
- How do you stop two deploys from racing?
Sign up free to read the full topic
More System Design topics
Start free on Cariara · Coding problems · System design studio