Cheat SheetsInterview Q&ACI/CD

CI/CD — Cheat Sheet

Interview Q&A · 100 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
CI/CD
Interview Q&A100 topicsQuick revision reference
1

What is continuous integration, and what does it actually require?

Continuous integration means every developer merges to the mainline frequently — at least daily — with an automated build and test run on every integration. The name describes the practice, not the tool. Running a build server while everyone works on branches for three weeks is not continuous integration; it is automated builds. What it requires: a single mainline everyone integrates into, small changes merged often, a build that runs automatically on every change, a test suite trustworthy enough that a green build means something, and a culture where a broken build is fixed immediately rather than worked around. The motivation is that integration pain grows superlinearly with divergence. Merging daily means small conflicts; merging monthly means a multi-day integration effort and a high chance of semantic conflicts that no merge tool catches. The practice that makes it possible for incomplete work is feature flags — code merged but inactive — which decouples integrating from releasing. The honest observation is that most organisations claiming CI are doing automated builds on long-lived branches, and the difference matters because the benefit comes from the integration frequency, not from the server.

2

What is the difference between continuous delivery and continuous deployment?

Continuous delivery means every change that passes the pipeline is releasable — built, tested, and packaged so it could go to production at any moment. The final push is a human decision. Continuous deployment removes that decision: every passing change goes to production automatically. The distinction is one manual gate, and whether you can remove it depends on confidence rather than on tooling. If your test suite genuinely catches regressions, if deployments are safe and reversible, and if you have monitoring that detects a bad release quickly, the gate adds delay without adding safety. If it does not, the gate is a human doing verification that automation should be doing — and it is a weak control, because a person approving twenty deploys a day is not meaningfully checking any of them. Continuous delivery is the more widely achievable target and is valuable on its own: being always releasable means you can ship a fix in minutes rather than assembling a release. The things that make continuous deployment viable are progressive delivery — canaries, feature flags — automated rollback, and observability good enough to notice a problem before users report it.

3

What makes a CI pipeline trustworthy?

That a red build reliably means something is broken, and a green build reliably means it is not. The thing that destroys trust is flakiness. A suite that fails randomly trains everyone to re-run rather than investigate, and once that habit forms a genuine failure is also re-run and eventually merged. A flaky suite is worse than no suite, because it consumes time and provides false assurance. So the highest-value work is usually fixing flaky tests rather than adding coverage. Speed is the second factor. A pipeline taking forty minutes changes behaviour: people batch changes, context-switch away, and stop running it locally. Under about ten minutes for the main feedback loop is the usual target. Determinism matters: the same commit should produce the same result, which means pinned dependencies, no reliance on external services, and no dependence on wall-clock time or execution order. And the failure output must be actionable. A red build whose log requires ten minutes of scrolling to understand gets ignored. The cultural half is that a broken mainline is stopped-the-line urgent, not something to work around.

4

What should run on every commit versus on merge to main?

On every commit or pull request: everything fast and everything that gates the merge. Linting, type checking, unit tests, and a build. The goal is feedback within a few minutes. Integration tests requiring a database or other services usually belong here too if they are fast enough, since catching integration failures before merge is much cheaper than after. On merge to main: the fuller suite — slower integration tests, end-to-end tests, security scanning, and the artefact build and publish. On a schedule rather than per-commit: very slow suites, dependency vulnerability scanning, performance benchmarks, and anything with a high false-positive rate that would otherwise block merges. The principle is to order checks by speed and by likelihood of failing, so the fast cheap ones fail first and developers get feedback quickly rather than after a twenty-minute wait. The anti-pattern is running everything everywhere, which makes the pull request loop slow and pushes people toward merging without waiting. And anything that only runs after merge must have a clear owner, because a failure there blocks everyone and belongs to nobody by default.

5

Why does pipeline speed matter so much?

Because it changes behaviour, not just throughput. A fast pipeline means a developer stays in context, sees the result, and fixes it immediately. A slow one means they switch to something else, come back an hour later, and have to reload the problem — which is far more expensive than the wall-clock time suggests. Slow pipelines also encourage batching: people accumulate changes rather than merging small ones, which reverses the benefit of continuous integration and makes every failure harder to attribute. And they create pressure to skip. A team waiting forty minutes starts merging without waiting, or adding exceptions, which erodes the whole control. The techniques that help: run independent jobs in parallel; cache dependencies aggressively, since dependency installation is frequently the largest single cost; only run what is affected by the change in a monorepo; use a shallow clone; and split the suite so fast feedback comes first. Profiling the pipeline is worth doing explicitly — the time is usually concentrated in one or two steps that nobody has looked at, often dependency installation or container image building.

6

What is a build artifact and why does immutability matter?

An artifact is the output of the build — a container image, a JAR, a binary, a package — that gets deployed. The principle that matters is build once, deploy everywhere. The same artifact is promoted through environments: built once, tested in staging, then the identical bytes go to production. Rebuilding per environment breaks that guarantee. Two builds from the same commit can differ — a dependency resolved to a new version, a different base image, a different toolchain — so what you tested is not what you shipped. That is a real and hard-to-diagnose class of incident. Immutability means an artifact is never modified after publication. A version number always refers to the same content, so a deployment is reproducible and a rollback is exact. The practices that follow: tag artifacts with the commit SHA, not just a semantic version, so you can always map a running deployment back to source. Never overwrite a published tag — mutable tags like latest make it impossible to know what is running. And keep environment-specific configuration outside the artifact, injected at deploy time, which is what makes one artifact deployable everywhere.

7

What does a reproducible build mean and why is it hard?

The same source produces bit-for-bit identical output, regardless of when or where it is built. It is hard because builds absorb ambient state. Timestamps embedded in archives. File ordering that depends on the filesystem. Absolute paths from the build directory. Locale and timezone. Non-deterministic compiler behaviour such as parallel code generation. And unpinned dependencies resolving to whatever is newest. The value is verification and trust. If anyone can rebuild and get the same artifact, you can prove the artifact corresponds to the source — which matters for supply chain security, since a compromised build server otherwise produces something nobody can detect. The practical steps toward it: pin every dependency with a lock file including transitive ones and hashes; pin base images by digest rather than tag; set SOURCE_DATE_EPOCH so timestamps are deterministic; sort file lists explicitly; and build in a container so the toolchain is fixed. Full bit-for-bit reproducibility is genuinely difficult and most teams do not achieve it. The achievable and still-valuable subset is dependency pinning and pinned base images, which eliminates the most common source of "it built differently today".

8

What belongs in version control and what does not?

Everything needed to build and deploy: source, tests, the pipeline definition, infrastructure as code, database migrations, and dependency lock files. The pipeline definition living in the repository is important — it versions with the code, so an old commit builds the way it did then, and pipeline changes go through review like any other change. Configuring pipelines through a web UI loses all of that. What does not belong: secrets, build outputs, dependencies themselves — the lock file rather than node_modules — large binaries, and anything generated. The grey area is environment configuration. Non-sensitive settings belong in the repository so they are reviewable and versioned; secrets go in a secret manager with only a reference committed. The test worth applying: could someone clone the repository and, with appropriate credentials, build and deploy the system? If something essential lives only in someone's head or in a UI configuration, that is a bus-factor risk and a reproducibility gap. And secrets that were ever committed must be rotated, because removing them from history does not undo the exposure.

9

What is the difference between a pipeline stage, a job, and a step?

The vocabulary varies by tool, but the structure is consistent. A step is a single command or action — run the tests, build the image. A job is a group of steps running together on one runner, sharing a workspace. Steps within a job run sequentially and share state. A stage groups jobs that run in parallel, with stages running in sequence. So a test stage might contain unit, integration and lint jobs running concurrently, and the deploy stage begins only when all of them pass. The practical implications. Jobs run on separate runners, so anything one job produces that another needs must be passed explicitly as an artifact or through a cache — assuming a file written in one job exists in the next is a common early mistake. Parallelism happens between jobs, so splitting work into jobs is how you make a pipeline faster. And failure semantics differ: a failed step usually fails its job, and a failed job usually fails the stage and stops later stages — though most tools allow continue-on-error for non-blocking checks such as an advisory lint.

10

What is the difference between a self-hosted and a managed CI runner?

A managed runner is provided by the CI service — a fresh ephemeral environment per job, maintained by the vendor. A self-hosted runner is a machine you operate that connects to the service and executes jobs. The reasons to self-host: access to private networks, so tests can reach an internal database; specific hardware such as GPUs or particular architectures; cost at high volume, since managed minutes are expensive at scale; and faster builds through persistent caches and warm dependencies. The costs are real. You maintain the machines, their software and their security. And the security model changes fundamentally: a self-hosted runner that is not ephemeral retains state between jobs, so a malicious or compromised job can leave something behind for the next one. That matters enormously for public repositories, where a pull request from anyone can run code on your runner. GitHub explicitly recommends against self-hosted runners on public repositories for this reason. The mitigation is ephemeral runners — a fresh container or VM per job, destroyed afterwards — which restores the isolation while keeping network access. That is the configuration worth insisting on.

11

How do you handle a monorepo in CI?

Build and test only what changed, or the pipeline time grows with the repository rather than with the change. The mechanism is change detection: determine which paths were modified and map them to affected projects, including anything depending on them. A change to a shared library must trigger its consumers, which is why a naive path filter is insufficient — you need the dependency graph. Tools that do this properly: Bazel, Nx, Turborepo, Pants. They model the graph and can also cache build outputs by input hash, so unchanged targets are not rebuilt at all — which is where the large wins come from. Without such a tool, path-based filters in the CI configuration work for loosely coupled projects and break silently when a shared dependency changes. The other monorepo concerns: shallow or partial clones, since cloning full history of a large repository is slow; and caching, which matters more because the dependency tree is larger. The trade-off worth stating: a monorepo gives atomic cross-project changes and one dependency version, at the cost of needing real build tooling. A polyrepo gives simple CI and a coordination problem instead.

12

What is a merge queue and what problem does it solve?

It solves the semantic conflict problem: two pull requests that each pass CI independently but break when combined. That happens because each was tested against main as it was when they branched, not against the state that results from merging both. A renamed method in one and a new caller in the other is the classic example — Git merges cleanly and the build breaks. Requiring branches to be up to date before merging addresses it, but on a busy repository that means constant rebasing and the last person to merge invalidates everyone else. A merge queue takes pull requests in order, tests each against the actual state that would result from merging everything ahead of it, and merges only if it passes. Failures are ejected from the queue without blocking the rest. That gives the guarantee without the rebase treadmill, and it is why GitHub, GitLab and others added the feature. The cost is latency — a change waits for those ahead of it — and it requires a reliable, reasonably fast pipeline, since a flaky suite ejects good changes. For a low-traffic repository, up-to-date branch checks are sufficient and simpler.

13

How would you structure a pipeline for a typical backend service?

Fast checks first, then progressively slower and more expensive stages, so failures surface as early and as cheaply as possible. A reasonable shape: lint and type check and unit tests in parallel — a few minutes, gating the pull request. Then build the artifact once. Then integration tests against real dependencies in containers. Then security scanning of dependencies and the image. On merge to main: publish the artifact tagged with the commit SHA, deploy to staging automatically, run smoke tests there, then deploy to production either automatically or behind an approval. The principles underneath. Build once and promote the same artifact, rather than rebuilding per environment. Keep configuration outside the artifact. Make every stage idempotent and re-runnable. And ensure a failure at any stage stops promotion. The things frequently missed: database migrations as an explicit ordered step rather than at application startup, so replicas do not race; smoke tests after deployment that actually exercise the service; and an automated rollback path that has been tested rather than assumed. And the pipeline definition lives in the repository so it versions with the code.

14

How do you make a pipeline faster?

Measure first, because the time is usually concentrated in one or two steps nobody has examined. The common wins. Cache dependencies — dependency installation is frequently the single largest cost, and a correctly keyed cache eliminates most of it. Key on the lock file hash so the cache invalidates exactly when dependencies change. Parallelise independent jobs, and split a large test suite across several runners. Use a shallow clone, since full history on a large repository is slow and CI rarely needs it. Build container images with layer caching and order the Dockerfile so dependencies are installed before source is copied — otherwise every source change invalidates the dependency layer. Only run what is affected, particularly in a monorepo. Use a faster runner; CPU time is usually cheaper than developer waiting time, and this is an underused lever. And remove work that does not earn its place — a check that has never caught anything and takes five minutes is worth deleting. The target worth aiming at is under ten minutes for the pull request loop, because that is roughly where people stop context-switching away.

15

How does caching work in CI and what are the pitfalls?

A cache stores a directory between runs, keyed by something that determines its validity — typically a hash of the lock file for dependencies. The key design is the whole problem. Too broad a key and you serve a stale cache, producing builds that work in CI and fail elsewhere, or that mysteriously use an old dependency version. Too narrow and it never hits. A restore-key fallback is the usual pattern: try the exact key, fall back to a prefix so a partially useful cache is reused and updated. The pitfalls. Caching build output rather than dependencies can mask a broken build, because stale artifacts are reused. Caching something that embeds absolute paths breaks when the runner differs. And a cache poisoned by a bad run persists until the key changes, which is why a manual cache-clear mechanism matters. The security concern is real on public repositories: a cache written by a pull request workflow could be read by a later privileged workflow, which is a supply chain vector. Scoping caches by branch mitigates it. And caches have size limits and eviction, so caching everything means caching nothing usefully.

16

What is the difference between a matrix build and parallel jobs?

A matrix generates several jobs from one definition by varying parameters — running the same tests across three Python versions and two operating systems produces six jobs. Parallel jobs are separately defined jobs that happen to run concurrently — lint, unit tests and a build running at the same time. So a matrix is for the same work across different environments; parallel jobs are for different work. The practical uses of a matrix: verifying support across language or dependency versions, testing on multiple platforms, and sharding a large test suite across N runners by passing the shard index as a matrix parameter. That last use is the one that matters most for speed, and it is underused. The cautions: a matrix multiplies quickly, and a three-by-three-by-two matrix is eighteen jobs consuming eighteen runners. Include and exclude let you prune combinations that are not meaningful. fail-fast, usually on by default, cancels the whole matrix when one job fails. That saves time but hides whether the failure is specific to one combination — turning it off is often more informative when diagnosing. And a matrix job that flakes takes the whole matrix red.

17

How should pipelines handle database migrations?

As an explicit, ordered step in the deployment — not at application startup. Running migrations at startup means several replicas race to apply them simultaneously, which most migration tools guard against with a lock but which still causes slow or failed startups. It also couples the migration to every restart. The safe ordering follows from backward compatibility. Every migration must work with both the currently-running code and the new code, because during a rolling deployment both are live. That means expand-and-contract for anything structural: add the new column, deploy code writing to both, backfill, deploy code reading the new one, then drop the old column in a later release. Several deployments, no downtime, no broken version. Adding a nullable column is safe; adding a non-null column, renaming, or dropping is not. The operational details: run migrations as a separate job before the application deploys, so a failure stops the rollout. Test migrations against a production-sized copy, since a statement that runs in a second on test data can lock a table for twenty minutes. And have a rollback plan, remembering that many migrations cannot be reversed without data loss.

18

What is pipeline as code and why does it matter?

The pipeline definition lives in the repository as a file rather than being configured through a web interface. The benefits follow from that. It versions with the code, so checking out an old commit gives the pipeline that built it. Changes go through review like any other change, so nobody silently disables a test. Differences between branches are possible, so a pull request can modify the pipeline and have that modification tested. And the whole thing is reproducible — a new repository can be set up from the file. UI-configured pipelines lose all of this: there is no history, no review, and no way to know what changed when a build starts behaving differently. The complications worth mentioning. YAML at scale becomes unwieldy, so most tools offer reusable components — composite actions, shared workflows, templates — and using them keeps definitions small. Some teams generate the YAML from a real programming language for the same reason. And a pipeline that can modify itself is a security consideration: on a public repository, a pull request changing the workflow file must not run with elevated permissions, which is why the distinction between pull_request and pull_request_target matters.

19

How do you handle environment-specific configuration?

Keep it out of the artifact and inject it at deployment, so one build can go to every environment. Baking configuration into the image means a separate build per environment, which breaks build-once-deploy-everywhere and means what you tested is not what you ship. The mechanisms: environment variables, a mounted configuration file, or a configuration service. In Kubernetes, ConfigMaps for non-sensitive values and Secrets for sensitive ones, mounted or projected into the pod. The practices that matter. Validate configuration at startup and fail fast with a clear message, so a missing value stops the deployment rather than producing a confusing error under load. Keep non-sensitive configuration in version control so it is reviewable, with only secrets held externally. Minimise the differences between environments — the more staging differs from production, the less staging tells you. And be careful with defaults: a setting that silently falls back to a development value in production is a real incident pattern, so required settings should have no default at all.

20

What is a deployment gate and when is a manual approval useful?

A gate is a condition that must be satisfied before promotion — automated checks, a time window, or a human approval. Manual approval is useful when there is genuine judgement to apply: a high-risk change, a release with business timing implications, or a change to a regulated system where sign-off is a compliance requirement. It is not useful as a general safety net. A person approving many deployments a day is not meaningfully reviewing any of them, so it becomes a delay that provides the appearance of control without the substance. That is a weak control and it is worth saying so. The better gates are automated: tests passing, security scans clean, canary metrics within bounds, error budget not exhausted. Progressive delivery replaces most of the value of a manual gate — deploying to a small percentage and automatically halting on bad metrics catches problems a human approval never would. The practical middle ground many teams reach: automatic deployment to production for most changes, with an approval required for migrations, infrastructure changes, or anything touching payments — where the judgement is real and the frequency is low enough that the approval means something.

21

How do you structure pipelines for multiple environments?

One pipeline that promotes a single artifact through environments, rather than a separate pipeline per environment. The artifact is built once and tagged, then deployed to development, staging and production in sequence, with gates between. Each deployment uses the same mechanism, so the production deployment path is the one exercised repeatedly rather than a special case used rarely. That last point matters: a deployment procedure used only for production is one nobody has practised, which is when things go wrong. The configuration differs per environment and is injected, not rebuilt. The environments themselves should be as similar as possible. A staging environment with a different database version, different instance sizes, or a fraction of the data tells you much less than it appears to, and the gap is where surprises live. The practical structure: environment definitions as infrastructure as code, so they are reproducible and their differences are visible in a diff. And ephemeral preview environments per pull request are increasingly common and genuinely valuable — reviewers can use the change rather than reading it, and the environment is destroyed on merge.

22

What should happen when the pipeline fails on main?

It should be treated as the highest-priority interruption, because main being broken blocks everyone. The immediate response is to restore green quickly — usually by reverting the offending commit rather than fixing forward, because a revert is fast and certain while a fix is an unknown amount of work under pressure. That convention needs to be explicit and blame-free, or people resist reverting their own change and the build stays red while they debug. The practices that support it: the person who broke it owns it, with the team helping; notifications go somewhere people actually see; and there is a norm that nobody merges onto a red main, since that compounds the problem and makes attribution harder. If main breaks often, that is a signal about the pull request pipeline — checks that should have caught it are missing, or the branch was not tested against current main. A merge queue addresses the latter. And a persistently red build that everyone has learned to ignore is the worst state, because the pipeline has stopped being a control while still costing time. That warrants stopping feature work to fix.

23

How do you version and tag releases in a pipeline?

Tag every artifact with the commit SHA, always. That is the identifier that unambiguously maps a running deployment back to source, and it never collides. Semantic versions are for consumers, communicating compatibility. They are appropriate for libraries and public APIs, and less meaningful for a continuously deployed internal service where nobody chooses which version to use. The automation options: derive the version from conventional commit messages, which lets tooling determine whether a change is a patch, minor or major and generate a changelog. Or use a date-based or build-number scheme where semantics are not needed. The practices that matter. Never overwrite a published tag — latest and other mutable tags make it impossible to know what is running, and are a real source of confusion during incidents. Tag the Git commit as well as the artifact, with an annotated tag, so the source of a release is findable. And embed the version and commit SHA in the application so it can report what it is — an endpoint returning the build identity is invaluable when you are trying to work out what is actually deployed.

24

What is GitOps?

The desired state of the system is declared in a Git repository, and an agent running in the cluster continuously reconciles reality to match it. The inversion from traditional CD is that the pipeline does not push to the cluster. It updates the repository — changing an image tag — and the agent pulls the change and applies it. Argo CD and Flux are the common implementations. The benefits. The repository is the audit trail: every change is a commit with an author and a review. Drift is detected and corrected automatically, so a manual change made during an incident is reverted or flagged. Rollback is a Git revert. And CI needs no cluster credentials, which removes a significant attack surface — a compromised CI system cannot deploy. That credential inversion is a strong security argument and is often the deciding factor. The costs: another component to operate, a learning curve, and the indirection makes debugging a stuck deployment less obvious. Secrets need a solution such as sealed secrets or an external operator, since they cannot be committed plainly. And the repository structure — one repository or many, per environment or per service — is a decision that is awkward to change later.

25

How do you test the pipeline itself?

It is code, so it deserves the same treatment, and this is routinely neglected. The practices that work. Keep the pipeline definition in the repository so changes to it are tested by the pull request they arrive in — a workflow change should trigger the workflow. Extract complex logic out of YAML into scripts, which can be unit tested and run locally. Anything beyond a few lines of shell inside a pipeline file is untestable and unmaintainable. Use local runners where available — act for GitHub Actions, or the tool's own local execution — to iterate without pushing, since the alternative is a commit-and-push loop that is slow and noisy. Lint the pipeline definition; most tools have a validator that catches schema errors before you push. Test the deployment path regularly by using it, which is an argument for deploying frequently — a deployment mechanism exercised daily is one that works, while one used monthly is a source of surprises. And test the rollback path deliberately, because it is the one nobody exercises and the one you need under pressure.

26

How do you handle a pipeline that needs to deploy several services together?

Prefer not to, because coupled deployments reintroduce the coordination problem that independent services exist to avoid. The better approach is backward-compatible changes so services can deploy independently in any order. A new API field is added before consumers use it; a removed field is stopped being read before it is removed. That is expand-and-contract applied across services, and it usually takes two or three releases rather than one coordinated one. Where a coordinated release is genuinely unavoidable — a breaking protocol change that cannot be staged — the mechanisms are a release train with a defined order, or feature flags that activate the new behaviour across services simultaneously once all are deployed. The flag approach is generally better because deploying and activating are separated, so the risky moment is a configuration change rather than a deployment, and it can be reversed instantly. The thing to avoid is a pipeline that deploys five services atomically, because a failure partway leaves an inconsistent state with no clean rollback. And if services must always deploy together, that is a signal they may not be separate services in any meaningful sense.

27

What is a flaky test and why is it worse than a failing one?

A test that passes sometimes and fails sometimes without the code changing. It is worse than a consistently failing test because a failing test gets fixed, while a flaky one gets re-run. Once re-running becomes the habit, a genuine failure is also re-run and eventually merged — so flakiness does not merely waste time, it disables the suite as a control. It also erodes trust broadly: a team that has learned the build is unreliable stops treating red as meaningful. The common causes: shared state making tests order-dependent, real time and dates, fixed sleeps waiting for async work, unmanaged randomness, network dependencies, and genuine races in the code under test. That last one matters — a flaky test is often reporting a real concurrency bug, so treating it as a test problem and adding a retry ships the bug. The diagnostic tools: run the test repeatedly, run the suite in random order to expose order dependence, and run in parallel to expose shared state. The policy that works: quarantine a flaky test so it runs but does not block, with a ticket and an owner — rather than leaving it failing intermittently or deleting it silently.

28

How should tests be split across a pipeline?

By speed and by what they gate, so fast feedback comes first. The rough layering. Static checks — lint, type check, format — in seconds, run first because they are cheap and catch a real class of error. Unit tests next: fast, no I/O, covering logic. These should be the bulk of the suite and should run in a couple of minutes. Integration tests against real dependencies in containers: slower, covering the wiring — that the query works, the serialisation is right, the migration applies. End-to-end tests last and fewest: expensive, slow and the most flaky, so they should cover a small number of critical user journeys rather than everything. That shape is the testing pyramid, and the failure mode is inverting it — many end-to-end tests and few unit tests, which gives a slow suite that fails often for reasons unrelated to the change. The practical splits: everything up to integration on the pull request; end-to-end and slower suites on merge or on a schedule; and performance and full security scans nightly. And shard the slow suites across parallel runners, which is usually the largest single speedup available.

29

How do you run integration tests that need a database?

Start a real database in a container for the test run — Testcontainers, or a service container in the CI configuration. Using a real engine matters. An in-memory substitute such as SQLite behaves differently — different SQL dialect, different constraint and locking semantics, different type handling — so tests pass and production fails. That is false confidence, which is worse than no test. The structure that works: start the container once per test session, run migrations against it, and give each test a transaction that is rolled back. That gives fast per-test isolation without recreating the schema. Running the migrations rather than creating tables from metadata means the migrations themselves are tested, which is worth having. The CI considerations: service containers need a readiness wait, because the container starting is not the database accepting connections — a fixed sleep is the flaky version, and polling for readiness is the correct one. And caching the database image saves pulling it every run. For parallel test execution, either give each worker its own database or ensure isolation by schema, or tests interfere and produce order-dependent failures.

30

What is code coverage good for and how is it misused?

Coverage measures which lines executed during tests. It is useful as a signal about untested areas, and it is a poor target. The misuse is treating a percentage as a goal. Coverage measures execution, not verification — a test that calls a function and asserts nothing produces full coverage and tests nothing. So a team pushed toward a number writes tests that raise it without improving confidence, and the metric stops meaning anything. It also creates the wrong incentives around code that is legitimately hard to test, encouraging tests that assert on implementation to reach the lines. What it is genuinely good for: finding entire files or branches with no coverage at all, which usually indicates something nobody thought about — error handling paths especially. And tracking that coverage does not fall sharply on a change, which is a reasonable gate. The better measure of suite quality is mutation testing: deliberately introducing faults and checking whether tests fail. That measures whether tests actually verify behaviour, which is the property coverage only proxies. So the practical use is coverage as a report to look at, with a ratchet preventing large drops, rather than a threshold to satisfy.

31

How do you handle tests that need external services?

Avoid depending on real external services in CI, because they make the pipeline slow, flaky and dependent on someone else's uptime. The options in order of preference. A fake or stub implementing the interface you depend on, which is fast and deterministic. Best when the interaction is simple. A recorded interaction — VCR-style — capturing real responses once and replaying them. That gives realistic payloads without the network, at the cost of recordings going stale silently when the real API changes. A local emulator, which several cloud providers offer for their services, and which is a good middle ground for storage and queues. Contract tests, which verify against the provider's published contract rather than against a guess, and which fail when the provider changes — solving the staleness problem that all mocking has. A small number of tests against the real service, run on a schedule rather than per-commit, to catch drift. The thing to avoid is per-commit tests hitting a third-party API: they fail for reasons unrelated to your change, which teaches people to ignore failures.

32

What are contract tests and when do you need them?

Contract tests verify that a provider still satisfies what its consumers actually depend on. The problem they address: with several services, integration tests spanning all of them are slow, brittle and require a full environment. But testing each service in isolation with mocks means a provider can change and break consumers without any test failing — because everyone's mocks still return the old shape. Consumer-driven contracts solve it. Each consumer publishes the subset of the API it uses, derived from its own tests. The provider's pipeline verifies it still satisfies every published contract, and fails if it does not — naming which consumer would break. Pact is the common tool. The lighter version, sufficient for many teams: commit the provider's OpenAPI schema and diff it in CI, failing on changes classified as breaking. That catches removed fields and changed types without requiring consumers to participate. You need contract testing when services are owned by different teams and deploy independently. Within one team deploying together, integration tests are simpler. The value is catching breakage at the provider's commit rather than in the consumer's production.

33

How do you keep end-to-end tests useful rather than a burden?

Keep them few, and cover only journeys where failure genuinely matters — sign-up, checkout, the core workflow. The reason to limit them: they are slow, they require a full environment, and they are the most flaky category by a wide margin because they depend on timing, network, browser behaviour and data state. A large end-to-end suite becomes the main source of pipeline unreliability. The practices that keep them workable. Never use fixed sleeps — wait for a condition with a timeout, since a sleep is both slow and flaky. Select elements by stable test identifiers rather than by CSS or text, which change constantly. Make each test create its own data and clean up, so tests are independent and can run in parallel. Run them against a deployed environment rather than a local composition, so they exercise something close to production. Retry once at most, and treat a test that needs retries as a bug to fix rather than a setting to tune. And have a clear owner, because an unowned end-to-end suite decays into something everyone ignores and nobody deletes.

34

What is the difference between smoke tests and full test suites in deployment?

A smoke test is a small set of checks run immediately after deployment to confirm the new version is fundamentally working — it starts, it responds, it can reach its database, a key endpoint returns sensible data. It is not comprehensive by design. Its job is to catch a catastrophic deployment quickly so you can roll back before users notice, so it must complete in seconds to a minute. The full suite is what runs before the artifact was promoted, verifying correctness. Running it against production after deployment is usually wrong: it is slow, it can create data, and it duplicates verification already done. The practical shape: smoke tests run automatically after each deployment, and a failure triggers automatic rollback. That closes the loop and is what makes automated deployment safe. What belongs in a smoke test: the health endpoint, a read path, a write path if it can be done safely with test data, and connectivity to each critical dependency. What does not: anything slow, anything mutating real user data, and anything that fails for reasons unrelated to the deployment. Synthetic monitoring is the continuous version of the same idea.

35

How do you test infrastructure as code?

In layers, because different failures surface at different stages. Static analysis first: terraform validate for syntax, and policy tools such as tfsec, Checkov or OPA for security and convention violations — an open security group, an unencrypted volume, a missing tag. That is fast and catches a real class of mistake. A plan against the target environment, reviewed as part of the pull request. Posting the plan output as a comment is standard practice and is the main review artifact — it shows exactly what would change, which the code alone does not. Then actual provisioning in a disposable environment, verifying the result works — Terratest and similar drive this. It is slow and expensive, so it usually runs on merge rather than per-commit. The things worth explicitly checking: that a destroy actually cleans up, or test environments accumulate cost; and that applying twice is a no-op, which verifies idempotence. Drift detection on a schedule catches manual changes made outside the code, which is a common source of the plan showing unexpected deletions later. And the plan-then-apply gate matters more than any test, because the plan is the last chance to notice a destroy.

36

How do you deal with a slow test suite you inherited?

Measure first — most test runners report per-test durations, and the time is usually concentrated in a small number of tests rather than spread evenly. Then the highest-leverage moves, roughly in order. Parallelise across runners, which requires tests to be independent and often exposes hidden shared state — a useful side effect. Fix fixture scope: recreating a database or an application context per test rather than per session is enormously wasteful, and a session-scoped resource with per-test transaction rollback is usually the single largest win. Remove sleeps, replacing them with polling for a condition. Split fast from slow with markers, so developers can run the fast set constantly and the full set runs in CI — a suite nobody runs locally has already lost most of its value. Move tests down the pyramid: a behaviour verified through an end-to-end test that could be a unit test is paying a hundred times the cost. And delete tests that assert nothing useful or duplicate others, which is politically harder than it should be but often justified.

37

What is shift-left testing?

Moving verification earlier — toward the developer and toward the moment the code is written — rather than concentrating it in a QA phase before release. The rationale is cost. A defect found while writing the code costs minutes. Found in code review, an hour. Found in a QA cycle, a day of context reloading. Found in production, an incident. In practice it means: type checking and linting in the editor, tests runnable locally in seconds, security scanning in the pull request rather than in a quarterly audit, and performance checks before release rather than after complaints. It also means developers own testing rather than handing it to a separate function, which is the organisational half and often the harder one. The caution worth stating is that shifting left does not mean testing only left. Some properties can only be verified in production or close to it — real traffic patterns, real data volumes, real dependency behaviour. Load testing, canary analysis and synthetic monitoring are shift-right practices, and they complement rather than compete. The balanced position is fast feedback early for correctness, and observation in production for the things that only appear there.

38

How do you prevent a pipeline check from becoming a rubber stamp?

Make it fail meaningfully, and remove it if it does not. A check that never fails provides no information but costs time on every run. A check that fails constantly for reasons nobody acts on trains people to ignore it, which is worse — it consumes attention and provides no signal. The symptoms of a rubber stamp: a linter with hundreds of suppressed warnings, a security scanner whose findings are all marked as accepted, a coverage gate set below current coverage, or a required approval given within seconds of the request. The responses. Tune the check so its findings are actionable — a scanner with a high false-positive rate needs its rules narrowed rather than its results ignored. Fix the backlog or accept it explicitly with an expiry rather than a permanent suppression. And delete checks that have never caught anything real. For human approval specifically, the fix is usually to make the automated checks strong enough that the approval is about judgement rather than verification, and to require it only where judgement is genuinely needed. The test to apply periodically: for each check, when did it last catch something that mattered?

39

How do you test a change that only manifests under load?

Load testing in an environment close enough to production for the result to mean something, which is the hard part. The requirements: realistic data volumes, since a query that is fast on a thousand rows may be catastrophic on ten million; realistic traffic shape, not a uniform rate; and comparable infrastructure, since a smaller instance changes where the bottleneck is. The measurement discipline matters. Drive at a fixed request rate rather than fixed concurrency, because fixed-concurrency tools produce coordinated omission — they stop sending while waiting, which hides the latency they should be measuring. Report percentiles, not averages. Run long enough for effects that take time to appear: connection pool exhaustion, memory growth, cache behaviour, garbage collection pressure. The alternatives when a realistic load environment is impractical. Shadow traffic — mirroring production requests to the new version without using its responses — gives real load with no user risk. And a canary at a small percentage exposes the change to real traffic with a bounded blast radius, which for many teams is the practical answer. The honest position is that some problems only appear in production, which is why progressive rollout matters.

40

What is the role of static analysis and linting in CI?

To catch mechanically-detectable problems automatically so that human review is spent on design and correctness rather than on style. The categories: formatting, which should be applied automatically rather than reported — a formatter in the pipeline that fails on unformatted code, plus a pre-commit hook, removes the entire conversation. Style and convention rules. Bug patterns that a linter can detect — unused variables, unreachable code, likely mistakes. Type checking. And security-focused analysis for injection risks and unsafe patterns. The practices that make it work. Fail the build rather than warn, or the warnings accumulate and are ignored. Configure once and enforce everywhere, so the editor, the pre-commit hook and CI agree — disagreement between them is a persistent irritation. Adopt incrementally on an existing codebase: enable rules gradually, or you get thousands of findings and disable the tool. And tune aggressively. A rule producing frequent false positives should be turned off, because its cost is the attention it consumes from every developer on every run. The payoff is review conversations about substance rather than about spacing.

41

How do you write an efficient Dockerfile for CI?

Order instructions so the layers that change least come first, because a changed layer invalidates every layer after it. The practical consequence: copy the dependency manifest and install dependencies before copying the source. If you copy everything first, every source change reinstalls dependencies — which is the single most common Dockerfile performance mistake and often accounts for most of the build time. Use a multi-stage build so build tools, compilers and test dependencies do not ship in the final image. That reduces size substantially and shrinks the attack surface, since a runtime image containing a compiler and a package manager is a much better place for an attacker to land. Use a minimal base — slim or distroless — for the same reasons. Pin the base image by digest rather than tag, so a rebuild does not silently pick up a different base. Use .dockerignore, or the build context includes .git, node_modules and test data, which slows every build. Run as a non-root user. And enable BuildKit with cache mounts for package manager caches, which persists them across builds without baking them into layers.

42

What is a multi-stage Docker build and why use one?

Several FROM instructions in one Dockerfile, where earlier stages build and a final stage copies only the needed output. The benefit is that the final image contains the artifact without the toolchain. A Java build needs a JDK and Maven — hundreds of megabytes — while running needs only a JRE and a JAR. A Go build needs the compiler; running needs a single static binary and possibly nothing else. The results are dramatic: images going from hundreds of megabytes to tens, or to a few for a static binary on a distroless or scratch base. The benefits beyond size. A smaller attack surface, since a runtime image with no shell, no package manager and no compiler gives an attacker very little. Faster pulls, which matters when scaling out. And no risk of build-time secrets persisting in the final image, since only explicitly copied files carry over. That last point is important: a secret used in an early stage does not appear in the final image, whereas in a single-stage build it persists in a layer even if a later command deletes it. Stages can also be targeted individually, which is useful for running tests in a build stage.

43

How do you handle build secrets without baking them into an image?

BuildKit secret mounts. The secret is mounted into the build for one command and is never written to a layer, so it does not persist in the image or its history. The wrong approaches and why. A build argument appears in the image metadata and is visible with docker history, so anyone who can pull the image can read it. An environment variable set in the Dockerfile persists in the layer. And copying a credentials file in and deleting it later does not help — the file remains in the earlier layer, and layers are all present in the image. That last one catches people repeatedly, because the running container genuinely does not have the file while the image demonstrably does. SSH agent forwarding into the build handles the common case of cloning a private repository without embedding a key. A multi-stage build also mitigates the problem, since a secret used in a build stage never reaches the final image. And the general principle: runtime secrets should never be in the image at all — they belong in the environment or a secret manager, injected at deployment, so the same image is deployable everywhere.

44

How should you tag container images?

Always with the commit SHA, because that is the unambiguous mapping from a running container back to source. A semantic version or a branch name can be reused; a SHA cannot. Additional tags for convenience — a semantic version for releases, a branch name for the latest build of a branch — are fine as long as the SHA tag exists. What to avoid is deploying by a mutable tag. Using latest, or a branch tag, means you cannot tell what is running, a rollback has no exact target, and two nodes pulling at different times can run different code. That is a genuinely bad failure mode during an incident. The stronger form is deploying by digest, which pins the exact image content regardless of tags — Kubernetes supports it and it removes the ambiguity entirely. The operational practices: an immutable tag policy on the registry so a published tag cannot be overwritten; a retention policy so old images are cleaned up, since registries grow without bound; and enough metadata in labels — commit, build time, source repository — that an image can be traced without external records. And sign images if the supply chain matters.

45

What is an artifact repository and why do you need one?

A store for build outputs — container images, packages, libraries — with versioning, access control and retention. The reasons to have one rather than rebuilding on demand. It enables build-once-deploy-everywhere: the artifact tested in staging is the exact one promoted to production. It gives a record of what was built and when. And it decouples build from deploy, so a deployment does not depend on the build system being available. It also acts as a proxy and cache for public dependencies, which matters more than people expect: it protects against an upstream package being removed or changed, gives you a copy if the public registry is down, and speeds up builds. That proxying is also a supply chain control — you can scan and approve what enters, rather than pulling arbitrary packages directly. The operational concerns: retention policies, because artifact storage grows continuously and old images are the bulk of it; access control, since the repository holds everything you deploy; and immutability, so a published version cannot be replaced. And for anything security-sensitive, provenance attestation recording how an artifact was built and from what source.

46

Why do dependency lock files matter for CI?

They pin the exact resolved versions of every dependency including transitive ones, so an install produces the same tree every time. Without one, a version range resolves to whatever is newest at build time. So two builds from the same commit can differ, a build that worked yesterday can break today with no code change, and what you tested is not necessarily what you shipped. That non-determinism is the single most common source of "it works on my machine". The practices: commit the lock file. Use the CI-specific install command that installs exactly the lock file and fails if it disagrees with the manifest — npm ci rather than npm install, which is the distinction people miss. Otherwise the install can silently update the lock file in CI, which defeats the purpose. Lock files with hashes additionally verify integrity, protecting against a registry serving different content for the same version. The trade-off is that pinned dependencies do not receive security updates automatically, so you need a process — Dependabot or Renovate opening pull requests — to update them deliberately, with the pipeline verifying each update. That is the right shape: pinned by default, updated deliberately, verified automatically.

47

What is an SBOM and why does it matter?

A Software Bill of Materials is a machine-readable inventory of everything in an artifact — every dependency, direct and transitive, with versions and licences. SPDX and CycloneDX are the common formats. Why it matters: when a vulnerability is announced in a widely-used library, the question is "are we affected, and where?" Without an inventory, answering that means manually inspecting every service, which during Log4Shell took organisations days or weeks. With SBOMs generated at build time and stored with each artifact, it is a query. It also supports licence compliance, which matters commercially — knowing you have not pulled in a copyleft dependency into a proprietary product. The practical implementation: generate it in the pipeline with a tool such as Syft, attach it to the artifact as an attestation, and store it where it can be queried. Scanning tools such as Grype consume it to report vulnerabilities without re-analysing the image. It is increasingly a procurement and regulatory requirement, which is driving adoption. The caveat is that an SBOM is only as good as the generation — dynamically loaded or vendored code can be missed, so it is a strong aid rather than a guarantee.

48

How do you handle a dependency with a known vulnerability?

Assess before reacting, because not every reported vulnerability is exploitable in your context. The questions: is the vulnerable code path actually reachable from your application? Is the affected functionality used? Is the component exposed to untrusted input? A vulnerability in a parsing function you never call is very different from one in your request path. Reachability analysis is what distinguishes tools that produce actionable findings from those that produce noise, and the noise problem is why scanner output is so often ignored. The responses, in order: upgrade to a patched version, which is usually straightforward and should be automated with Dependabot or Renovate. If no patch exists, look for a workaround — a configuration change disabling the affected feature. Replace the dependency if it is unmaintained. Or accept the risk explicitly with an expiry date and a documented rationale, rather than a permanent suppression nobody revisits. The policy that works: fail the build on critical and high severity in production dependencies, warn on the rest, and keep the suppression list small and reviewed. And distinguish production from development dependencies, since a vulnerability in a test tool is not the same risk.

49

What is the difference between a build and a release?

A build produces an artifact from source. A release makes a specific artifact available to users. Separating them is what makes build-once-deploy-everywhere possible: many builds happen, a subset are promoted through environments, and a smaller subset become releases. The deeper separation worth naming is between deploying and releasing. Deploying puts the code on the infrastructure; releasing exposes the behaviour to users. Feature flags separate them, so code can be deployed inactive and enabled independently — which means the risky moment is a configuration change that can be reversed in seconds rather than a deployment that takes minutes to roll back. That separation is what makes continuous deployment compatible with careful feature launches. The practical implications: a release has a version, a changelog and a record; a build has an identifier. A release may aggregate several builds. And a rollback of a release is not necessarily a rollback of a deployment — turning a flag off may be sufficient and is much faster. Conflating the two is what makes teams afraid to deploy, because every deployment is treated as a user-visible event.

50

How do you manage versioning for a library versus a service?

A library needs semantic versioning, because consumers choose which version to use and must know what upgrading implies. Major means breaking, minor means additive, patch means fixes. Honouring that contract is what makes dependency ranges safe. A continuously deployed service does not have that problem — there is one version running and nobody chooses it — so a commit SHA or a build number is sufficient. Applying semantic versioning to it is ceremony without benefit, though a human-readable version can help communication. The practices for a library: automate version determination from conventional commit messages so the bump matches the change, generate a changelog, and publish from the pipeline rather than from a developer machine, which removes the credential from laptops and makes the process reproducible. Deprecate before removing, with a deprecation warning in a minor release and removal in the next major. And for a service, the useful additions are embedding the version and commit in the binary so it can report its identity, and a tag on the Git commit so releases are findable. The common mistake is versioning a service like a library and then never incrementing the major.

51

What is supply chain security in a CI context?

Protecting the path from source to running artifact, since compromising the build system compromises everything it produces — which is what made SolarWinds so damaging. The attack surfaces. Dependencies: a malicious package, a typosquatted name, or a compromised maintainer account. Build tools and actions, which are themselves dependencies and often pulled by a mutable tag. The build environment, particularly a shared self-hosted runner. And the artifact between build and deployment. The mitigations. Pin dependencies with lock files and hashes, and pin CI actions to a commit SHA rather than a tag — a tag can be moved, so an action pinned to v3 can change under you. Use ephemeral, isolated runners so nothing persists between jobs. Scope credentials minimally and prefer short-lived tokens over long-lived secrets — OIDC federation to a cloud provider removes stored credentials entirely. Generate provenance attestations recording how an artifact was built, and sign artifacts so consumers can verify. And review dependency updates rather than auto-merging them, since an automated update of a compromised package is exactly the vector.

52

Why should you pin CI actions and base images by digest?

Because tags are mutable and digests are not. An action referenced as v3, or a base image as node:20, resolves to whatever the publisher currently points that tag at. If the publisher moves it — or if their account is compromised — your pipeline runs different code with no change on your side and no record of it. That is a real attack path: a popular action compromised and its tag repointed executes in every pipeline using it, with access to that pipeline's secrets. Pinning to a commit SHA for actions, or an image digest for containers, means you run exactly what you reviewed. An update becomes an explicit change in a pull request. The cost is that updates no longer arrive automatically, including security fixes — so it needs Dependabot or Renovate configured to open pull requests for pinned digests, which both tools support. That combination is the right shape: pinned by default so nothing changes silently, with automated pull requests so updates are visible, reviewable and tested. The same reasoning applies to any third-party script fetched and executed in a pipeline, which is a surprisingly common pattern and worth avoiding entirely.

53

What is a rolling deployment and what can go wrong?

Instances are replaced gradually — a few at a time — so the service stays available throughout. It is the default in Kubernetes and most orchestrators. The advantages: no extra infrastructure, no downtime, and a gradual exposure so a catastrophic failure affects only part of the fleet before health checks stop the rollout. What goes wrong. Old and new versions run simultaneously, so both must be compatible — with each other, with the database schema, and with any shared state. A change that is not backward compatible breaks during the window, which is why expand-and-contract migrations matter. Rollback is slow, because it is another rolling deployment in reverse rather than an instant switch. And the health check determines everything. A check that passes before the application is genuinely ready means traffic reaches instances that cannot serve it, and the rollout continues past a broken version. Readiness must reflect actual readiness, including dependency connectivity. The other frequent problem is graceful shutdown: if the termination grace period is shorter than the longest request, every deployment drops requests — which appears as intermittent errors correlated with releases.

54

What is blue-green deployment?

Two complete environments. Blue serves production; green is idle. You deploy the new version to green, verify it, then switch traffic — usually at the load balancer or DNS — in one step. The advantages: rollback is switching back, which is near-instant and is the main reason to choose it. Only one version serves traffic at a time, so there is no mixed-version window. And you can test green fully before it receives any real traffic. The costs. Double the infrastructure for the duration, which is expensive for a large fleet. The switch is all-at-once, so if the new version has a problem that only appears under real load, every user hits it simultaneously. That is the trade against a canary, which exposes a fraction. Shared state is the real complication. The database is not duplicated, so both versions must work with one schema — the backward compatibility requirement does not go away. In-flight sessions, connection pools and caches also need thought. And the idle environment must be kept genuinely current, or the first deployment to it after a long gap fails for unrelated reasons.

55

What is a canary deployment and how do you decide whether to proceed?

The new version receives a small fraction of traffic — one percent, then five, then more — while metrics are compared against the existing version. The value is bounded blast radius. A bad release affects a small number of users and is caught before wide exposure, which is the strongest argument for it over blue-green. The decision to proceed should be automated, comparing the canary against the baseline on error rate, latency percentiles, and business metrics such as conversion or successful transactions. Manual observation does not scale and is unreliable at low traffic. The requirements that make it work. Enough traffic for statistical significance — a canary at one percent on a low-traffic service takes hours to accumulate meaningful data, so canaries suit high-volume services. Comparable baselines: comparing the canary against the whole fleet is confounded by which users are routed there, so comparing against an equivalently-sized control group is better. And automated rollback on breach, with a defined bake time before each promotion. Argo Rollouts and Flagger implement this against Prometheus metrics, which is where most teams get it rather than building it.

56

How do feature flags change deployment?

They separate deploying from releasing. Code goes to production inactive, and the behaviour is enabled by a configuration change. That changes the risk profile fundamentally. The deployment is no longer the risky event — it ships dormant code. The risky event is the flag flip, which takes effect in seconds and can be reversed in seconds, without a rollout. It also enables gradual exposure independent of deployment: enable for internal users, then one percent, then everyone. And targeting — a specific customer, a region, a plan tier. That is what makes trunk-based development work with incomplete features: merge continuously, activate when ready. The costs are real and frequently underestimated. Every flag is a branch in the code, so N flags mean up to 2^N combinations, most of them never tested. Flags left in place after a feature is permanent accumulate into an unmaintainable tangle. So the discipline is a lifecycle: each flag has an owner and a removal date, and removing it is part of finishing the feature rather than optional cleanup. And flags used for long-lived configuration are a different thing and should be named differently.

57

What is a shadow or dark launch?

Production traffic is mirrored to the new version, but its responses are discarded — the user is served by the existing version and never sees the shadow. The value is testing against genuine production traffic — real request shapes, real data, real volume — with zero user risk. That catches performance problems and edge cases that synthetic load testing misses, because real traffic is far more varied than anything you would generate. It is particularly good for validating a rewrite or a major refactor: run both, compare outputs, and find where they disagree before switching. The complications are what limit its use. Side effects must be suppressed — the shadow must not write to the database, send emails, or charge cards, so it either needs a separate data store or careful gating. Getting that wrong causes duplicate side effects, which is the failure mode to be careful about. It doubles the load on downstream dependencies unless they are also shadowed. And comparing responses requires the outputs to be deterministic enough that differences are meaningful, which they often are not — timestamps and identifiers differ legitimately. Service meshes make the traffic mirroring itself straightforward.

58

How do you achieve zero-downtime deployment?

Several things must all hold, and missing any one produces dropped requests. Graceful shutdown: on SIGTERM the instance stops accepting new connections, finishes in-flight requests, and exits. The termination grace period must exceed the longest request, or requests are killed. Readiness must be accurate: an instance should only receive traffic when it can actually serve it, including having warmed connections and caches if that matters. And it must fail readiness before shutdown begins, with a brief pause, because removal from the load balancer is not instantaneous — traffic can arrive for a second or two after SIGTERM, which is the most common cause of errors at the very start of termination. Backward compatibility: old and new versions run simultaneously, so the schema, the API and any shared state must work with both. Expand-and-contract for schema changes. Connection draining at the load balancer. And the rollout must be gradual with health-check gating, so a broken version stops the rollout rather than replacing everything. The test is deploying under load and watching for errors — most teams discover their deployment is not actually zero-downtime this way.

59

How do you roll back safely?

By having a tested, automated path — because the moment you need it is the worst moment to be improvising. For stateless code, redeploying the previous artifact is straightforward, and blue-green makes it a traffic switch. The hard part is the database. Many migrations cannot be reversed without data loss: a dropped column's data is gone, and a type change may not be invertible. So the rollback plan must be designed with the migration, not after. That is the strongest argument for expand-and-contract — if every schema change is additive and backward compatible, the old code still works against the new schema, and rolling back the application requires no database change at all. That is what makes rollback safe. The practices: never combine a destructive migration with the deployment that stops using the column — separate them by at least one release. Keep the previous version's artifact available. And practise rollback, because a path never exercised does not work. The alternative worth naming is rolling forward: for a small bug, deploying a fix can be faster than rolling back, particularly if the pipeline is quick. Which is right depends on severity and confidence.

60

What is expand and contract for database changes?

A pattern for making schema changes without downtime, by splitting a breaking change into a sequence of compatible steps. For renaming a column: add the new column; deploy code that writes to both and reads the old; backfill existing rows; deploy code that reads the new column; stop writing the old one; and finally drop it in a later release. Each step is compatible with the code running before and after it, so at no point does a rolling deployment have a mixed state that breaks. That is several deployments for one logical change, which people resist — but the alternative is downtime or a broken window. The general rules: adding a nullable column is safe. Adding a non-null column requires adding it nullable, backfilling, then adding the constraint. Removing anything requires first removing all references from code and deploying that. The operational cautions: backfilling a large table must be batched, or it locks the table or blows up the transaction log. And adding an index or a constraint can lock — PostgreSQL supports CREATE INDEX CONCURRENTLY and NOT VALID constraints validated separately for exactly this reason. Test migrations against production-sized data.

61

How do you deploy to multiple regions?

Progressively, one region at a time, with verification between — never everywhere at once. The reason is blast radius. A simultaneous global deployment means a bad release takes out every region, which turns a degradation into a total outage. Sequencing means the first region is the canary and the rest are protected. The usual order: a low-traffic region first, verify, then progressively larger ones, with a bake time between each. The complications. Data replication means a schema change in one region affects others if they share a database or replicate between them, so schema changes must be applied compatibly across the whole topology before any region runs the new code. Version skew across regions is a real state — a request routed to one region and a follow-up to another may hit different versions, so cross-region compatibility matters just as within-region compatibility does. And the deployment mechanism itself should be regional, so a failure of the deployment system in one region does not block others. The monitoring requirement is per-region metrics, or a problem in one is averaged away in a global view.

62

What is progressive delivery?

The umbrella term for releasing gradually with automated verification — canaries, feature flags, ring deployments, and traffic splitting — rather than switching everything at once. The underlying idea is that testing before release can never be complete, because production has traffic patterns, data and dependency behaviour that no test environment reproduces. So instead of trying to be certain before release, you release in a way where being wrong is cheap. The components: gradual exposure so few users are affected; automated analysis comparing the new version against a baseline on real metrics; and automatic rollback on breach. Ring deployments are the coarser version — internal users, then early adopters, then everyone — which works when traffic is too low for a statistically meaningful canary. The requirements are observability good enough to detect a regression automatically, and metrics that reflect user experience rather than just infrastructure health. Without those, gradual exposure just means a slower bad release. The cultural shift is treating production as part of the verification process rather than the end of it — which is the shift-right counterpart to shift-left testing, and the two are complementary.

63

How do you handle deployment of a stateful service?

Much more carefully, because the usual assumption that instances are interchangeable does not hold. The differences: instances have identity and persistent storage, so replacement is not free. Ordering matters — a database cluster usually needs a specific sequence, often replicas before the primary. And data must survive the deployment, so storage is decoupled from the instance lifecycle. Kubernetes StatefulSets provide stable identities, stable storage, and ordered rolling updates for this reason. The practices. Deploy one instance at a time with verification between, rather than in parallel. Check cluster health before proceeding — replication caught up, quorum maintained — since replacing a second node before the first has resynchronised can lose quorum. Handle leader election explicitly: a rolling update of a cluster with a leader should update followers first and fail over deliberately, rather than letting the leader be replaced unexpectedly. Take a backup before, and verify it is restorable rather than assuming. And the honest answer for databases specifically is that a managed service handles this better than most teams will, and the operational burden of self-managing is frequently underestimated.

64

What health checks does a deployment need?

Liveness and readiness, and they must be distinguished — conflating them causes outages. Liveness asks whether the process is broken and should be restarted. It should check almost nothing beyond the process responding. If liveness checks a database and the database has a brief problem, every instance fails liveness and the orchestrator restarts the entire fleet, converting a blip into a full outage. Readiness asks whether this instance should receive traffic. It may check dependencies, because removing an instance from rotation is reversible and harmless. A startup probe is worth adding for a service with slow initialisation, so liveness does not kill it during a long start. The implementation details. Keep checks cheap, since they run constantly across every instance. Exclude them from access logs and latency metrics, or they dominate and skew the percentiles. Fail readiness before shutdown begins, so traffic drains before the process stops accepting connections. And make readiness meaningful: a handler returning 200 unconditionally tells you nothing, and is functionally the same as having no check while giving the appearance of one.

65

What is the difference between imperative and declarative deployment?

Imperative describes the steps: run this command, copy these files, restart this service. Declarative describes the desired end state and lets a system reconcile toward it. The practical differences. A declarative definition is idempotent — applying it twice changes nothing the second time — whereas an imperative script must handle "already done" cases explicitly, and usually does so badly. Declarative systems detect and correct drift, so a manual change made during an incident is reverted or flagged. Imperative scripts have no notion of current state, so drift accumulates invisibly. And a declarative definition is reviewable as a diff showing what will change, which is what makes terraform plan and kubectl diff valuable. The cost is a learning curve and less obvious debugging — when the reconciler is not converging, understanding why is harder than reading a script. Kubernetes manifests, Terraform and Ansible playbooks are declarative; a bash deployment script is imperative. The pragmatic position is that declarative wins for infrastructure and deployment because idempotence and drift detection matter enormously at scale, and imperative remains fine for genuinely one-off operations.

66

How do you deploy configuration changes safely?

Treat them with the same care as code, because a configuration change can break production just as thoroughly and usually faster. The practices: version configuration in Git so changes are reviewed and have history. Validate at startup so a malformed value fails the deployment rather than producing a confusing error later. And roll out gradually rather than applying globally at once. That last point is frequently missed. A configuration change pushed to every instance simultaneously has no blast radius containment — which is why several large outages have been caused by a config push rather than a code deploy. For dynamic configuration that changes without a deployment — feature flags, limits, routing rules — the same gradual approach applies, and the system should validate values before applying them. The risky category is anything that changes behaviour globally and instantly: a connection pool size, a timeout, a rate limit, a routing rule. Those deserve a canary as much as code does. And have a rollback path: the previous configuration should be recoverable, which is another argument for versioning it. Static configuration baked into an artifact is safer but less flexible; the trade is deliberate.

67

What is immutable infrastructure?

Servers are never modified after deployment. To change anything, you build a new image and replace the instance rather than patching in place. The benefits. No configuration drift: every instance is identical because they came from the same image, which eliminates the "works on that server but not this one" class of problem entirely. Deployments are reproducible and rollback is replacing instances with the previous image. And the build is tested as a unit, so what you verified is what runs — rather than a base image plus an unknown sequence of subsequent changes. The practices: build the image in the pipeline, test it, and roll it out. Never SSH into a production server to fix something, because that instance is now different from its peers and the fix disappears on the next replacement. That discipline is the hard part culturally, since fixing a server directly during an incident is the natural instinct. Containers make this the default, which is much of why they were adopted. For VMs, Packer building AMIs is the equivalent. The things that must be externalised: state, logs and configuration, since none of them can live on an ephemeral instance.

68

How do you decide between rolling, blue-green and canary?

By what you are optimising for and what you can afford. Rolling is the default: no extra infrastructure, no downtime, gradual. Choose it when the change is low risk and rollback speed is not critical. Its weakness is slow rollback and a mixed-version window. Blue-green when instant rollback matters more than infrastructure cost, and when you want to verify the new version fully before any real traffic reaches it. Good for a release with a hard go or no-go decision. Its weakness is the all-at-once switch and doubled cost. Canary when you have enough traffic for meaningful metric comparison and you want the smallest blast radius. It is the safest for high-risk changes, and it costs the most in tooling and time. The practical considerations beyond the theory: traffic volume, since a canary needs enough requests to be statistically meaningful; infrastructure cost, since blue-green doubles it; and whether the change is backward compatible, since rolling and canary both require it. And feature flags cut across all three — with a flag, the deployment strategy matters less because the risky change is the flag flip. Most teams use rolling by default and canary for high-risk changes.

69

What makes a good container image for production?

Small, minimal and non-root. Small through a multi-stage build so the toolchain does not ship, and a minimal base — slim, alpine, or distroless. Smaller images pull faster, which matters when scaling out, and they have fewer packages to carry vulnerabilities. Non-root, with a dedicated user, and a read-only root filesystem where possible. A container running as root that escapes its isolation is root on the host. No shell and no package manager if you can manage it, which is what distroless provides — an attacker who lands in the container has very little to work with. Pinned base image by digest, so rebuilds are reproducible. Proper signal handling: the process must be PID 1 via exec form, or use an init, so SIGTERM reaches it and graceful shutdown works. One concern per image — not a process manager running several services, which breaks the orchestrator's model of health and restart. Labels recording the commit and build metadata so an image can be traced. And no secrets in any layer, remembering that deleting a file in a later layer does not remove it from the image.

70

How does Docker layer caching work and how do you use it well?

Each instruction creates a layer, and layers are cached by the instruction plus the content it depends on. A changed layer invalidates every layer after it. So ordering determines cache effectiveness. Put things that change rarely first and things that change often last. The canonical application: copy the dependency manifest and install dependencies, then copy the source. A source change then reuses the cached dependency layer. Copying everything first means every source change reinstalls dependencies, which is the single most common and most expensive Dockerfile mistake. COPY invalidates based on file content, so copying a directory containing frequently-changing files invalidates that layer constantly — which is what .dockerignore is for. RUN invalidates on the command string, so a command fetching something from the network is cached even when the remote content changed, which can produce stale builds. In CI the cache is often cold because each job starts fresh, so you need an external cache — registry-based caching with BuildKit, or a persistent cache mount — or none of this helps. BuildKit cache mounts also let package manager caches persist without baking them into layers.

71

How do you scan container images for vulnerabilities?

A scanner such as Trivy, Grype or Snyk inspects the image, identifies the OS packages and application dependencies, and matches them against vulnerability databases. Where it runs matters. In the pipeline before publishing, so a vulnerable image is not shipped. And continuously against images already deployed, because new vulnerabilities are announced in code that has not changed — an image clean at build time is not clean forever, and that continuous scan is what most teams skip. An admission controller can also block deployment of unscanned or failing images. The policy question is what to fail on. Failing on every finding is unworkable, because base images routinely carry unfixable low-severity issues and the build becomes permanently red. Failing on critical and high with a fixed version available is a reasonable line. The noise problem is the main practical challenge: a scanner reporting hundreds of findings gets ignored entirely, so tuning matters more than coverage. The most effective reduction is not triage but a smaller base image — distroless has almost nothing to report, which removes most findings by removing the packages. And keep an allowlist with expiry dates rather than permanent suppressions.

72

What is image signing and provenance?

Signing produces a cryptographic signature over an image so consumers can verify it came from your pipeline and has not been altered. Cosign is the common tool. Provenance goes further: an attestation recording how the image was built — which source commit, which builder, which inputs — so you can verify not just who published it but how it was produced. SLSA defines levels of assurance for this. The threat addressed is supply chain compromise. Without verification, an attacker who gains registry access can replace an image with a malicious one and nothing detects it, because deployment pulls by tag and trusts what it receives. With signature verification enforced at admission, an unsigned or wrongly-signed image is rejected. Keyless signing using OIDC identity — signing with the pipeline's workload identity rather than a stored key, recorded in a transparency log — removes the key management problem, which was the main barrier to adoption. The practical steps: sign in the pipeline, publish the attestation alongside the image, and enforce verification in the admission controller. Signing without enforcing verification provides no protection, which is a common half-measure.

73

What is a registry retention policy and why does it matter?

Rules governing how long images are kept and which are deleted. It matters because registries grow without bound. Every pipeline run publishes an image, so a busy repository accumulates thousands, most of which will never be deployed again. That costs storage and, on hosted registries, real money — and it makes the registry slower to browse and manage. The policy needs to balance cleanup against the ability to roll back. Deleting an image you might need to redeploy is a genuine risk, so the usual shape is: keep all tagged releases indefinitely; keep the last N images per branch; delete untagged images after a short period; and delete images from deleted branches. The caution is that an image currently deployed must never be deleted, which requires either checking what is running or keeping anything referenced by a deployment. The related consideration is immutability: a tag policy preventing overwrite means an image cannot be replaced, which is what makes a digest reference trustworthy. And garbage collection in self-hosted registries is often a separate manual operation — deleting the manifest does not reclaim the blobs until it runs, which surprises people whose storage does not shrink.

74

How do you build container images inside a container?

The problem is that building normally requires the Docker daemon, and giving a build container access to the host daemon by mounting its socket effectively grants root on the host — a container that can start containers can mount the host filesystem. That is the docker-in-docker socket-mounting pattern, and it is a significant privilege escalation in a shared CI environment. The alternatives. Kaniko builds images from a Dockerfile without a daemon and without privileged mode, running as a normal container. BuildKit can run in rootless mode. Buildah similarly builds without a daemon. Privileged docker-in-docker runs a full daemon inside the container, which avoids sharing the host daemon but requires privileged mode — which is also a substantial grant, though the blast radius is the container rather than the host. The practical guidance for a shared runner: Kaniko or rootless BuildKit. For an ephemeral single-tenant runner, mounting the socket is a smaller risk since the runner is destroyed afterwards, but it is still worth avoiding. The other consideration is caching, which is harder without a persistent daemon — registry-based cache export is the usual answer.

75

Why should containers run as non-root?

Because container isolation is not a security boundary you should rely on absolutely. It is namespaces and cgroups sharing one kernel, so a kernel vulnerability or a misconfiguration can allow escape — and a process running as root in the container is root on the host when that happens. Running as a non-root user means an escape lands as an unprivileged user, which is a substantially smaller problem. It also limits damage within the container: an attacker cannot install packages, modify system files, or write outside their own directories. The implementation: create a user in the Dockerfile and set USER before the entry point. Ensure the application does not need to write anywhere it lacks permission, which is the usual source of friction. The complications. Binding a port below 1024 requires either a capability or, better, using a higher port and mapping it — there is no reason for a container to listen on 80 internally. Files copied into the image are owned by root by default, so ownership needs setting. And a read-only root filesystem with explicit writable volumes is the stronger form, which also catches applications writing where they should not.

76

What is the difference between CMD and ENTRYPOINT, and why does it matter for signals?

ENTRYPOINT is the fixed executable; CMD provides default arguments to it, or the command itself if no entry point is set. A command passed to docker run replaces CMD but not ENTRYPOINT. The part that matters operationally is the form. Exec form — a JSON array — runs the binary directly as PID 1. Shell form — a plain string — runs it under /bin/sh -c, so the shell becomes PID 1 and your process is its child. That matters because the shell does not forward signals. SIGTERM reaches the shell, the application never sees it, and the container is SIGKILLed after the grace period — so graceful shutdown never happens and every deployment drops in-flight requests. The symptom is intermittent errors correlated with deployments, with nothing in the application logs, which is genuinely hard to attribute. So the rule is exec form for anything that should shut down cleanly. The related issue is that being PID 1 means the kernel applies no default signal handling, so the application must install a SIGTERM handler explicitly — ignoring it is the default. And if the process spawns children, use an init such as tini to reap them.

77

How do you handle logs from containers?

Write to stdout and stderr, and let the platform collect them. That is the twelve-factor approach and it is right because the container filesystem is ephemeral — logs written to a file disappear when the container is replaced, and they fill the node in the meantime with no rotation. The platform handles collection: the container runtime captures the streams, and an agent ships them to a log store. The practices that make them useful. Structured JSON rather than free text, because logs are queried by machines and parsing rules on free text break constantly. A correlation ID on every line, propagated through the request and across services, or you cannot reconstruct what happened. Meaningful levels so consumers can filter. No secrets, remembering that logs are widely accessible and retained. The operational concerns: log volume is a real cost, so health check requests and debug-level output in production need consideration. And a node's disk can still fill if the runtime's log rotation is not configured — the default is often unbounded, which has taken down nodes. And logs are not metrics: counting things by querying logs is expensive and slow compared to a counter.

78

What resource requests and limits should a container have?

Requests are what the scheduler reserves; limits are the hard ceiling. Memory: set requests and limits, usually equal. Exceeding the memory limit means an OOM kill, so the limit must accommodate the real footprint — which for a JVM or a runtime with native allocations is meaningfully more than the heap. Setting the limit equal to the heap size guarantees eventual kills. CPU: set requests, and think carefully about limits. A CPU limit causes CFS throttling — the process is descheduled entirely for the remainder of each 100ms period once the quota is exhausted — which produces latency spikes while average utilisation looks low. For latency-sensitive services many teams set requests without CPU limits, letting them burst into idle capacity. The counter-argument is that unlimited containers can starve neighbours, so it depends on how the cluster is shared. The sizing method is measurement: run under realistic load, observe actual usage, and set requests near the typical and limits with headroom for spikes. And remember requests drive scheduling and bin-packing, so systematically over-requesting wastes cluster capacity while under-requesting causes evictions.

79

How should secrets be handled in a pipeline?

Stored in the CI system's secret store or an external secret manager, injected as environment variables or files at run time, and never committed. The practices that matter beyond that. Scope them narrowly: a secret available to every job and every branch is available to any pull request, including one from an outside contributor. Scoping by environment and requiring approval for protected environments is the control. Prefer short-lived credentials over stored ones. OIDC federation to a cloud provider means the pipeline exchanges its identity token for temporary credentials, so there is no long-lived key to leak — that is the single biggest improvement available and it eliminates the rotation problem entirely. Mask them in logs, which CI systems do for known secrets — but masking is best-effort and fails if a secret is transformed, base64-encoded, or split. Rotate on a schedule and have a tested rotation procedure. And audit which jobs access which secrets, since a pipeline with broad access is a high-value target — compromising CI compromises everything it can deploy to.

80

What is OIDC federation for CI and why is it better than stored credentials?

The CI system issues a short-lived identity token asserting facts about the run — the repository, the branch, the workflow. The cloud provider is configured to trust that issuer and exchange the token for temporary credentials scoped to a role. The benefit is that no long-lived credential exists anywhere. There is no access key stored in the CI system to leak, to rotate, or to be exfiltrated by a malicious pull request. The credentials issued are short-lived, typically minutes, so even interception has a narrow window. And the trust policy can be specific: only the main branch of a particular repository may assume the production deployment role. That means a pull request cannot obtain production credentials even if it can run arbitrary code. That conditional scoping is the part most worth getting right, and the common mistake is a trust policy matching the repository but not the branch or environment — which grants any branch the same access. The setup is a one-time configuration of the identity provider and role trust policies. It is supported by GitHub Actions, GitLab and others against AWS, GCP, Azure and Vault, and adopting it should be a default for any new pipeline.

81

What are the security risks of running CI on pull requests?

A pull request contains code the author controls, and CI executes it. For a public repository that means anyone on the internet can run code in your pipeline. The risks: exfiltrating secrets available to the job; using your runners for cryptomining; poisoning a cache that a later privileged job reads; and modifying the workflow file itself to grant additional access. The defences. Do not expose secrets to pull request workflows from forks — most CI systems default to withholding them, and the default should be preserved. Understand the distinction between the event that runs with the fork's code and no secrets, and the one that runs with the base repository's context and secrets. GitHub's pull_request versus pull_request_target is exactly this, and using pull_request_target while checking out the pull request's code is a well-known and serious vulnerability — it combines untrusted code with privileged context. Require approval before running workflows for first-time contributors. Use ephemeral runners so nothing persists between jobs, and never self-hosted runners on public repositories without that. And scope the pipeline token to read-only where it does not need to write.

82

How do you prevent secrets being committed?

Layered detection, because any single control is bypassable. A pre-commit hook running a scanner such as gitleaks or detect-secrets catches most cases at the earliest and cheapest point. The weakness is that hooks are local and can be skipped with --no-verify, so it is a convenience rather than an enforcement. A CI check running the same scanner on the diff is the enforcement layer, failing the build. Server-side scanning — GitHub secret scanning and equivalents — detects known credential formats after the fact and can notify the provider to revoke automatically, which is genuinely valuable because revocation is faster than any human process. A pre-receive hook on the server is the strongest, since it rejects the push outright. The structural prevention is making it unnecessary: configuration from environment variables and secret managers, with .env files gitignored and an .env.example committed showing the shape without values. And the critical response when one does get committed: rotate first, because removing it from history does not undo the exposure — anyone who cloned, any CI log, any cache may have it. History rewriting is the second step, not the first.

83

What permissions should a pipeline have?

The minimum for what it does, scoped per job rather than per pipeline. The common failure is one broadly-privileged credential used by every job, so a test job that needs nothing can deploy to production. That means compromising the least sensitive part of the pipeline compromises everything. The better structure: read-only tokens for jobs that only build and test; write access to the registry only for the publish job; deployment credentials only for the deploy job, scoped to one environment; and production access gated behind a protected environment requiring approval. With OIDC, the trust policy can encode this directly — the production role assumable only from the main branch of a specific repository. The default token deserves attention too. Most CI systems grant a token with broad repository permissions by default; setting it to read-only and granting specific permissions per job is a meaningful reduction that costs nothing. And third-party actions run with whatever the job has, so a job with a write token running an unpinned action is trusting that action with write access — which is the supply chain concern restated as a permissions problem.

84

What is SAST, DAST and SCA, and where do they belong?

SAST is static application security testing — analysing source code for vulnerable patterns such as injection, unsafe deserialisation or hardcoded credentials. It runs early, in the pull request, and is fast. Its weakness is false positives, which is what causes teams to disable it. SCA is software composition analysis — scanning dependencies for known vulnerabilities. This is usually the highest-value of the three, because most vulnerabilities in a typical application come from dependencies rather than from its own code. It belongs in the pipeline and on a schedule, since new vulnerabilities are announced in unchanged code. DAST is dynamic testing — probing a running application for vulnerabilities from the outside. It finds things static analysis cannot, such as configuration and runtime issues, but it is slow and needs a deployed environment, so it belongs against staging on a schedule rather than per-commit. The practical ordering: SCA first because it is the highest yield and lowest noise, then SAST tuned narrowly, then DAST if you have the environment. And the failure mode for all three is noise — a tool producing findings nobody acts on is worse than none, because it consumes attention and provides false assurance.

85

How do you secure a self-hosted runner?

Make it ephemeral, which addresses most of the risk. A fresh container or VM per job, destroyed afterwards, means nothing a job leaves behind can affect the next one — no poisoned tooling, no lingering credentials, no modified caches. Without that, a persistent runner shares state between jobs from potentially different trust levels, which is the fundamental problem. Beyond ephemerality: never run self-hosted runners on public repositories, where anyone can submit a pull request that executes on them. GitHub explicitly warns against this. Isolate the runner network so it can reach only what it needs — a runner with broad network access into production is a lateral movement path. Run the runner process as a non-privileged user, and avoid mounting the Docker socket, using Kaniko or rootless BuildKit instead. Keep the runner software updated, since it is internet-facing in effect. And separate runner pools by trust level: a pool for pull requests with no production access, and a separate pool with deployment credentials that only protected branches can use. Sharing one pool means the least trusted workload runs alongside the most privileged credentials.

86

What is the risk of third-party CI actions and how do you manage it?

A third-party action is code executing in your pipeline with access to that job's secrets, tokens and filesystem. It is a dependency with the same supply chain risk as any other, but it runs in a more privileged context. The specific risks: a compromised maintainer account publishing a malicious version; a mutable tag being repointed so you silently run different code; and an action that transitively pulls other actions you never reviewed. The controls. Pin to a full commit SHA rather than a tag, so what you reviewed is what runs — a tag can be moved, a SHA cannot. Pair that with Dependabot or Renovate opening pull requests for updates, so pinning does not mean stagnating. Prefer actions from the CI vendor or well-known publishers, and review what an unfamiliar action actually does before adopting it. Minimise the job's permissions and secrets, so a compromised action gets little. An allowlist of permitted actions is available in some CI systems and is worth enabling for an organisation. And for anything trivial, a few lines of shell you control is often better than a dependency.

87

How do you handle compliance requirements in a pipeline?

By making the pipeline the evidence rather than producing evidence separately. Most compliance requirements — change control, segregation of duties, testing, approval, traceability — map onto controls a good pipeline already has. Change control is the pull request with review. Segregation of duties is requiring an approver other than the author, and requiring that deployment goes through the pipeline rather than by hand. Traceability is the commit SHA linking artifact to source to ticket. Testing evidence is the pipeline record. The key is that these are enforced by the system rather than by policy, so they cannot be skipped and the evidence is automatic. A control that depends on people following a documented process produces neither reliability nor evidence. The additions usually needed: immutable audit logs of who approved and deployed what; artifact provenance; and retention of pipeline records for the required period. The failure mode to avoid is bolting on a manual approval and a spreadsheet, which satisfies an auditor while slowing everything and improving nothing. And emergency change procedures need defining in advance, or the first incident produces an undocumented bypass.

88

What should never be logged by a pipeline?

Secrets in any form, and anything that reveals them indirectly. The direct cases: environment variables printed wholesale, which is a common debugging habit and dumps every secret into a log that is often world-readable on a public repository. Command lines containing credentials, since many tools accept a token as an argument and the command is echoed. And curl or database commands with embedded credentials. CI systems mask known secret values, but masking is best-effort: it fails if the secret is transformed, base64-encoded, split across lines, or constructed at runtime. So masking is a safety net, not a control. The indirect cases: full request and response bodies containing personal data or tokens; stack traces that include configuration; and generated files echoed for debugging. The practices: never print the environment, use tools' file-based or environment-variable credential mechanisms rather than command-line arguments, and turn off shell tracing in any step handling secrets — set -x is a frequent accidental leak. And treat pipeline logs as potentially public, especially on a public repository, because they are retained and often accessible more broadly than people assume.

89

How do you handle an emergency deployment safely?

With a defined path that is faster but not unaccountable, decided before the incident rather than during it. The temptation under pressure is to bypass the pipeline entirely — deploy by hand, skip tests, push directly. That is how a fix makes things worse, and it leaves no record. The better design is a break-glass path through the same pipeline with reduced gates: skip the slow end-to-end suite but keep the build and the fast tests, allow a single approver instead of two, but still produce an artifact, still tag it, and still record who did it. That keeps traceability and the safety of a real build while removing the parts that cost time. The supporting practices: the path must be documented and, ideally, practised, because a procedure first used during an outage is not a procedure. Access to it should be logged and reviewed afterwards. And a follow-up is required — reverting the shortcut, adding the test that would have caught it, and reviewing the change properly. The deeper point is that if the normal path is slow enough to need bypassing regularly, the normal path is the problem.

90

What is the blast radius of compromising a CI system?

Effectively everything it can deploy to, which for most organisations is production. A CI system typically holds deployment credentials for every environment, registry write access, and the ability to modify what runs. So compromising it means being able to publish a backdoored artifact that passes every control, because the controls run inside the compromised system. That is why SolarWinds was so damaging: the build system was the target precisely because everything downstream trusts its output. The implications for how you treat it. CI is production infrastructure and deserves the same protection — access control, monitoring, patching, and network isolation — rather than being treated as developer tooling. The reductions available: OIDC with narrowly scoped, short-lived credentials rather than stored long-lived keys. Separate runner pools by trust level. Least-privilege tokens per job. Signed artifacts with verification at deployment, so an artifact not produced by the legitimate pipeline is rejected. And provenance attestation so the build path is verifiable. And GitOps inverts the direction — the cluster pulls rather than CI pushing — so CI needs no cluster credentials at all, which removes the most valuable target.

91

What are the DORA metrics and what do they tell you?

Four measures from the DevOps Research and Assessment programme: deployment frequency, lead time for changes, change failure rate, and time to restore service. The finding that makes them interesting is that speed and stability correlate positively rather than trading off. High-performing teams deploy more often and have fewer failures, because small frequent changes are easier to verify, easier to diagnose and easier to reverse. That contradicts the intuition that deploying less often is safer, and it is the main argument for continuous delivery. What each tells you. Deployment frequency and lead time measure throughput — how quickly work reaches users. Change failure rate and time to restore measure stability. The practical use is as a diagnostic rather than a target. A long lead time points at where the pipeline or the process is slow. A high change failure rate points at testing or at change size. A long restore time points at rollback and observability. The caution is Goodhart's law: measured as targets, they are gameable — deployment frequency rises by splitting deployments, and failure rate falls by not recording incidents.

92

How do you know a deployment succeeded?

Not by the pipeline reporting green, which only means the deployment mechanism completed. The verification layers. Smoke tests immediately after, exercising a few critical paths against the deployed version, with failure triggering rollback. That catches a catastrophically broken release within seconds. Then metrics comparison: error rate, latency percentiles and throughput compared against the pre-deployment baseline over a bake period. A deployment that raises the error rate should be reverted automatically rather than waiting for someone to notice. Business metrics matter as much as technical ones — a release where every request succeeds but conversions drop is a failure that error rates will not show. Synthetic monitoring gives continuous verification of key journeys independent of real traffic. And the deployment should be visible in monitoring — annotating dashboards with deployment events is a small thing that makes correlation obvious, because the first question about any anomaly is whether something was deployed. The practice that closes the loop is automatic rollback on breach, because a verification nobody acts on is just a dashboard.

93

How do you correlate a production problem with a deployment?

Make deployments visible in the same place as the metrics. The practical mechanisms: annotate dashboards with deployment events, so a graph shows exactly when each release happened and a step change is immediately attributable. Emit a deployment event to your observability platform from the pipeline. Tag metrics, logs and traces with the version or commit SHA, so you can compare the new version against the old directly rather than looking at an aggregate that blends both. During a rolling deployment that is the only way to see that the new version is failing while the old is fine. Have the application report its own version through an endpoint and in its logs, so you can confirm what is actually running rather than what you believe was deployed. And keep a deployment log — what was deployed, when, by whom, and the commit range — which is the first thing anyone asks during an incident. The reason this matters is that "what changed?" is the highest-yield question in any incident, and deployments are the most common answer. Making that correlation instant rather than a manual investigation is a large reduction in time to restore.

94

What is automated rollback and when should it trigger?

The deployment system reverts to the previous version automatically when defined conditions are breached, without waiting for a human. The triggers: smoke tests failing after deployment; error rate exceeding a threshold relative to the baseline; latency percentiles degrading; health checks failing on the new instances; and for a canary, the automated analysis rejecting it. The design considerations. The thresholds must account for normal variance, or you get spurious rollbacks — comparing against a control group is more robust than against an absolute number. There needs to be a bake period, since a problem may take minutes to appear, and rolling forward immediately after a rollback creates a loop. And rollback must be safe, which is the constraint that limits it — if the deployment included an irreversible migration, automatic rollback can make things worse. That is another argument for expand-and-contract, since it keeps rollback always safe. A rollback should also alert loudly rather than silently reverting, because a system that quietly undoes deployments can hide a persistent problem. The payoff is time to restore measured in seconds rather than in however long it takes someone to notice.

95

What should be monitored about the pipeline itself?

Duration, per stage as well as overall, so you can see where time goes and notice regression. A pipeline that has crept from eight minutes to twenty-five changes behaviour, and it usually happens gradually enough that nobody notices. Success rate, split by cause where possible: genuine test failures are a healthy signal, while infrastructure failures and flaky tests are noise to eliminate. A pipeline whose failures are mostly infrastructure has stopped being informative. Flaky test rate specifically, tracked per test, so the worst offenders are visible and can be prioritised — this is the metric that most directly predicts whether people trust the suite. Queue time waiting for a runner, which is invisible in the job duration but is real waiting. Deployment frequency and lead time, which are the DORA throughput measures and reflect the pipeline's effect on delivery. And cost, since CI minutes and runner infrastructure are a real line item that grows silently. The practice worth adopting is treating the pipeline as a product with users — the developers — and measuring their experience of it rather than only whether it functions.

96

How do you debug a pipeline failure that does not reproduce locally?

Enumerate the differences, because the failure lives in one of them. Environment: different OS, different language version, different installed tools. Pinning the runner image and using containers for the build removes most of this. Dependencies: a lock file not being respected, so CI resolved different versions. Using the strict install command rather than the permissive one is the fix. Environment variables: something set locally in a shell profile and absent in CI, or vice versa. Timing and concurrency: CI machines are often slower and more contended, so race conditions and timing-dependent tests fail there and not locally. That is a genuine bug being exposed rather than a CI problem. Ordering: CI may run tests in a different order or in parallel, exposing shared state. State: a local machine has caches, a database with data, and files from previous runs that a fresh runner does not. The techniques: increase log verbosity, print the environment excluding secrets, and use an SSH-into-the-runner debug session if the CI system supports it — which is by far the fastest route when available. And reproducing in the same container image locally eliminates most variables at once.

97

What is a post-incident review and how does it relate to the pipeline?

A blameless analysis after an incident, focused on the systemic conditions that allowed it rather than on who made a mistake. The pipeline connection is that many incidents trace back to a gap in it: a test that did not exist, a check that was skipped, a deployment that was not gradual, a rollback that was not automated, or an alert that did not fire. So the most valuable output is usually a change to the pipeline rather than a change to the code — adding the test that would have caught it, making the deployment gradual, automating the rollback. The blameless framing matters practically, not just culturally: if people expect blame, they under-report and describe incidents less accurately, so the analysis is worse. The premise is that a system permitting a single mistake to cause an outage has a design problem. The questions worth asking: how long until we knew, how long until we understood, how long until we fixed it — and what would shorten each. And the actions need owners and dates, because the common failure is a thorough review whose actions are never done, which means the same incident recurs.

98

How do you reduce the time to restore service?

Attack each phase: detect, diagnose, fix. Detection: monitoring on user-facing symptoms rather than only infrastructure, with alerts that fire on error rate and latency. Synthetic checks catch failures even with no traffic. The goal is knowing before users report it. Diagnosis: deployment annotations on dashboards so "what changed" is answered instantly, since deployments are the most common cause. Distributed tracing to localise a failure to a service. Structured logs with correlation IDs. Fix: automated rollback, which is the largest single lever — reverting in seconds rather than debugging under pressure. That requires rollback to be always safe, which requires backward-compatible schema changes. Feature flags shorten it further, since turning a flag off is faster than any deployment. The supporting practices: small frequent deployments, so the change set to investigate is small; runbooks for known failure modes; and practising the recovery path so it works when needed. The framing worth giving is that restore time is more controllable than failure rate. You cannot prevent all failures, but you can make recovery fast and routine — which is where the resilience actually comes from.

99

How do you introduce CI/CD to a team that has none?

Incrementally, delivering value at each step, rather than designing the ideal pipeline and building it for months. A workable order. First, get the build running automatically on every push — even with no tests, that catches "it does not compile" and establishes the habit. Then add whatever tests exist, and make failures block merging. If there are no tests, add them for the next change rather than trying to retrofit coverage. Then automate the artifact build so it is reproducible. Then automate deployment to a non-production environment, which is where the deployment mechanism gets exercised safely. Then production deployment, initially with a manual trigger. The sequencing principle is that each step must be visibly useful, or momentum dies. And automating deployment to staging before production means the risky mechanism is proven before it matters. The obstacles are usually not technical: a test suite nobody trusts, a deployment process that only one person knows, and reluctance to deploy frequently. Addressing those is the real work. And start with the service that deploys most often, since it has the most to gain and provides the clearest demonstration.

100

What distinguishes a good pipeline from one that merely works?

Trust and speed, more than features. A good pipeline is one people believe: red means broken, green means safe, and nobody re-runs a failure hoping it passes. That property is fragile and is destroyed by flakiness faster than by anything else, which is why fixing flaky tests usually beats adding coverage. It is fast enough that developers wait for it rather than context-switching away — roughly ten minutes for the pull request loop. It fails informatively, so the reason is obvious from the output rather than requiring a log excavation. It makes the safe path the easy path: deploying through the pipeline is quicker than deploying by hand, so nobody is tempted to bypass it. And it makes deployment boring — frequent, small, reversible, and unremarkable enough that nobody schedules a meeting for it. The measures that reflect this are the DORA four, and the thing they capture is that speed and safety are not opposed: small frequent changes with fast rollback are both faster and safer than large infrequent ones. A pipeline that merely works produces artifacts. A good one changes how the team behaves.

Learn this free with Aria, your AI tutor → AiCanCode.org/learn/interview