Continuous delivery
Keep every change releasable: deployment pipelines, environments, configuration kept out of code, and database changes that never need downtime.
- Distinguish continuous integration, continuous delivery and continuous deployment
- Design a deployment pipeline through environments with automated gates
- Separate configuration from code and evolve databases with expand and contract
Byte Bakery’s CI is green all day. But getting a green build to customers still means waiting for Release Night. Continuous delivery closes the gap.
| Practice | Means |
|---|---|
| Continuous integration | every change is merged to main and built and tested automatically |
| Continuous delivery | every change that passes the pipeline is releasable - deploying is a push-button business decision |
| Continuous deployment | every change that passes the pipeline is deployed to production automatically, no button |
The backbone is the deployment pipeline: the artifact from CI moves through environments - say dev → staging → production - and each stage adds confidence with automated checks (acceptance tests, performance tests, security scans, smoke tests after each deploy). Manual approvals are allowed, but each one is a queue; replace them with automated checks wherever you can.
Config is not code
The same artifact runs everywhere, so anything that differs between environments must live outside it. The Twelve-Factor App says: store config in the environment. Database URLs, log levels and feature flag defaults come from environment variables or config files mounted at deploy time; secrets (passwords, API keys) come from a secret manager (Vault, AWS Secrets Manager, Kubernetes Secrets), never from the repository.
A common layering: defaults in a base file, overridden by an environment file, overridden by environment variables.
1base = {"log_level": "info", "oven_url": "http://localhost:9000", "drone_batch": "10"}
2staging = {"oven_url": "http://oven.staging.internal"}
3environment = {"BYTE_LOG_LEVEL": "debug", "HOME": "/home/baker"}
4
5config = {**base, **staging}
6for name, value in environment.items():
7 if name.startswith("BYTE_"):
8 config[name.removeprefix("BYTE_").lower()] = value
9print(config){'log_level': 'debug', 'oven_url': 'http://oven.staging.internal', 'drone_batch': '10'}Try it
Code or config?
Which of these belong in the code (the same in every environment), and which in configuration?
“How a loyalty discount is calculated”
“The database URL”
“The drone API key”
“The log level”
“The retry-with-backoff algorithm”
“How many server replicas to run”
Database changes without downtime
During a deploy, old and new versions of the app run at the same time, against the same database. A migration that drops or renames a column the old version still uses breaks it mid-deploy. The expand and contract (parallel change) pattern avoids that:
- Expand: add the new column or table (nullable, or with a default). Old code ignores it.
- Migrate: deploy code that writes both and reads the new; backfill old rows.
- Contract: once no running code uses the old column, drop it - in a later release.
Key takeaways
CI keeps main green; continuous delivery keeps it releasable; continuous deployment releases it automatically.
A deployment pipeline promotes one artifact through environments, with automated gates.
Keep config in the environment and secrets in a secret manager.
Change databases with expand, migrate, contract - old and new code run side by side.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: automate DevOps chores in Python
Write the small Python tools DevOps teams really build - pipeline runners, plan checkers, metric calculators, scanners - and run them against sample inputs. They run locally in your browser; no servers or cloud accounts needed.
Resolve layered configuration
The first input line is the environment name, like staging. Then come sections: [base], one section per environment, and [env] (environment variables), each followed by key=value lines.
The effective config starts from [base], is overridden by the chosen environment’s section, then by environment variables starting with BYTE_ (strip the prefix and lowercase the rest; ignore other variables). Print every key alphabetically as key = value (source), where the source is base, the environment name or env. Mask values whose key contains password, secret or key as ****.
- Staging
- Production
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Check migrations for zero downtime
Each input line is a migration statement. Classify each one (case-insensitive) for a deploy where old and new code run side by side:
DROP COLUMNorDROP TABLE:UNSAFE (old code still uses it - drop it in a later release)RENAME COLUMNorRENAME TO:UNSAFE (old code uses the old name - add, copy, switch, then drop)ADD COLUMNwithNOT NULLand noDEFAULT:UNSAFE (old code inserts rows without it)CREATE INDEXwithoutCONCURRENTLY:warning (locks writes while it builds - use CONCURRENTLY)- anything else:
safe
Print each statement as 1. safe: CREATE TABLE ... or 2. UNSAFE: ALTER TABLE ... (old code inserts rows without it) - the label, the statement, then the reason in parentheses - and finally verdict: safe to deploy, verdict: deploy with care (1 warning) or verdict: split this migration (2 unsafe).
- Risky release
- Careful release
- Index only
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…