04 ยท Software Engineering
CI/CD & DevOps โ from commit to production, deeply
You run services on K8s and ship via pipelines, so this is a "prove you understand what you use" topic. Most candidates can name Docker and K8s; few can explain layer caching, or why a Service needs an Ingress. The why behind each layer is the differentiator.
1First principles: what problem does CI/CD solve?
Integration risk grows superlinearly with batch size. Merge two weeks of changes at once and you get combinatorial conflicts plus a huge haystack when something breaks. CI/CD is the discipline of shrinking batches:
- CI (Continuous Integration): merge to main frequently; every merge is automatically built + tested, so breakage is detected within minutes of introduction โ while the diff is small and the author still has context.
- CD (Continuous Delivery): every green build is deployable โ packaging, config, migrations all automated; a human pushes the button. Continuous Deployment removes the button.
2Pipeline anatomy
A canonical backend pipeline, stage by stage:
push/PR
โ lint + format + typecheck # seconds; fail fastest on the cheapest checks
โ unit tests # fast, parallelized
โ build artifact (docker image) # build ONCE; same bytes flow to every env
โ integration tests # against real containerized deps (Postgres, Kafka)
โ security scans # dep audit, image CVEs (Trivy), secret detection
โ push image to registry # tagged with git SHA โ immutable, traceable
โ deploy โ staging # automatic
โ smoke tests / e2e on staging
โ deploy โ prod # gated (approval) or automatic
โ post-deploy verification # health checks, error-rate watch, auto-rollback
Principles to articulate:
- Fail fast, cheapest first. Lint before tests before builds โ most feedback in least time.
- Build once, promote everywhere. Never rebuild per environment โ rebuilds can differ (new base image, new transitive dep). Promote the same image digest from staging to prod. Config differs per env; the artifact doesn't. (This is 12-factor build/release/run โ ยง6.)
- Immutable, traceable artifacts. Tag images with the git SHA, not
latestโ "what exactly is running in prod?" must have a one-word answer. - Pipeline speed is a feature. Over 10โ15 min to green and people batch changes โ exactly the anti-pattern CI fights. Tools: dependency caching, test parallelization/sharding, path filters (don't run the full suite for a README change), tiered suites (fast per-commit, full pre-merge/nightly).
- Deployment strategies: rolling (replace pods gradually โ K8s default, zero infra cost), blue-green (two full environments, switch traffic atomically, instant rollback, 2x cost), canary (route 1โ5โ25โ100% of traffic while watching error rates โ safest for high-traffic services).
3GitHub Actions โ the concrete tool
Model: a workflow (YAML in .github/workflows/) is triggered by events (push, pull_request, schedule, workflow_dispatch) and runs jobs (parallel by default, each on a fresh runner VM; sequence with needs:); each job runs steps โ shell commands or reusable actions.
name: ci
on:
pull_request:
push: { branches: [main] }
jobs:
test:
runs-on: ubuntu-latest
services: # sidecar containers for integration tests
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12", cache: "pip" } # dep caching
- run: pip install -e ".[dev]"
- run: ruff check . && ruff format --check .
- run: pytest -x -q --cov
build-and-push:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write } # least-privilege token
steps:
- uses: actions/checkout@v4
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/org/app:${{ github.sha }}
cache-from: type=gha # layer cache across runs
cache-to: type=gha,mode=max
Details that signal real use:
- Secrets via
${{ secrets.X }}โ encrypted, masked in logs, scoped per repo/environment. Better still: OIDC federation to cloud providers โ the runner exchanges a short-lived identity token for cloud credentials, so no long-lived cloud keys sit in GitHub at all. Name-dropping OIDC here is high-value. - Matrix builds (
strategy: matrix: python: [3.11, 3.12]) โ one job definition, N parallel variants. - Environments with required reviewers = the manual gate before prod.
- Concurrency groups (
concurrency: { group: deploy-prod, cancel-in-progress: false }) โ serialize deploys; cancel superseded PR runs to save minutes.
pull_request_target + untrusted PR code = classic secrets-exfiltration vector. Also pin third-party actions to SHAs. Supply-chain awareness reads very well โ and it's Sentinel-adjacent: your product consumes GitHub events, so you've already thought about trusting webhook-driven automation.
4Docker, deeply
The core idea
A container is not a VM. It's a normal Linux process, isolated by kernel features:
- Namespaces โ the process gets its own view of PIDs, network, mounts, hostname, users.
- cgroups โ CPU/memory limits.
- Union filesystem โ its own filesystem assembled from image layers.
No guest OS, no hypervisor โ millisecond starts, near-zero overhead. All containers share the host kernel โ which is also the isolation caveat vs VMs (one sentence of security awareness).
What Docker actually solved: "works on my machine." The image packages app + runtime + system deps + filesystem into one immutable, content-addressed artifact that runs identically on a laptop, CI, and prod โ dev/prod parity as an artifact property.
Images and layers
An image is an ordered stack of read-only layers; each Dockerfile instruction that changes the filesystem creates one. Layers are content-addressed and shared โ 10 images on python:3.12-slim store the base once. At runtime the container adds a thin writable layer on top (copy-on-write) โ which is why container-local writes are ephemeral and disposable.
Multi-stage builds
Build-time tooling (compilers, headers, dev deps) shouldn't ship to prod โ size, attack surface, CVE noise. Multi-stage: build in a fat stage, COPY --from= only the outputs into a slim final stage.
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt # needs gcc etc. โ stays here
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app/ app/
RUN useradd -m appuser
USER appuser # non-root: container escape โ root on host
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Hygiene checklist to rattle off: slim/distroless base; .dockerignore (.git, .env, caches โ build-context bloat and secret leaks); non-root USER; exec-form CMD (JSON array โ your process is PID 1 and receives SIGTERM directly, which is what makes K8s graceful shutdown work); never ARG/ENV secrets into an image โ they persist in layer history (docker history shows them); use BuildKit --mount=type=secret for build-time credentials; pin image digests for reproducibility.
5Kubernetes โ core objects and why each exists
K8s in one sentence: a declarative reconciliation engine โ you write desired state into the API server; controllers run a loop comparing desired vs actual and act to converge. Everything below is "what desired state can I declare?"
Smallest deployable unit: one or more containers sharing a network namespace (same IP, localhost between them) and volumes โ one app container + optional sidecars. Why it exists: the atomic unit of scheduling. Pods are cattle โ mortal, disposable, rescheduled freely; you almost never create bare pods. Everything else exists because pods die.
Declares "N replicas of this pod template"; manages ReplicaSets. Why it exists: rolling updates โ a new ReplicaSet scales up while the old scales down (maxSurge/maxUnavailable tune the choreography); kubectl rollout undo points back at the previous ReplicaSet โ rollback is built in. Siblings: StatefulSet (stable identity + per-pod storage for DBs), DaemonSet (one pod per node, for agents), Job/CronJob (run-to-completion work).
Stable virtual IP + DNS name (http://api.default.svc) load-balancing over pods selected by labels. Why it exists: pods die and change IPs โ callers depend on a name while membership stays dynamic. This is service discovery. Types: ClusterIP (internal, default), NodePort (port on every node), LoadBalancer (cloud LB per service โ expensive for everything).
The L7 front door: one entry point routing by host/path to Services, with TLS termination. Why it exists: instead of a cloud LoadBalancer per service. An Ingress controller (nginx, Traefik) actually implements it; modern successor: Gateway API โ a phrase worth knowing.
Config outside the image (12-factor): ConfigMaps for plain config, Secrets for credentials โ injected as env vars or mounted files. Honest caveat interviewers like: Secrets are base64-encoded, not encrypted by default โ real hardening = etcd encryption at rest, RBAC on reads, and/or an external manager (Vault, cloud stores) via External Secrets Operator. Mounted-file secrets can rotate without pod restarts; env-var secrets can't.
Control loop scaling a Deployment's replicas between min/max on metrics (CPU/memory or custom โ queue depth, requests/sec). Crucial dependency: HPA math runs off resource requests, so pods must declare resources.requests/limits โ requests drive scheduling (bin-packing); limits cap usage (memory over limit โ OOMKill). Vertical: VPA; node-level: cluster autoscaler/Karpenter.
Ingress routing, concretely:
rules:
- host: api.example.com
http:
paths:
- path: /
backend: { service: { name: api, port: { number: 80 } } }
The glue that makes it all work: probes
- Liveness โ "restart me if this fails" (hung process).
- Readiness โ "don't send me traffic yet/now" (warming up, dependency down). Readiness gating is what makes rolling deploys zero-downtime: new pods receive traffic only when actually ready.
- Startup โ patience for slow-booting apps before liveness kicks in.
terminationGracePeriodSeconds โ else SIGKILL. Narrating this end-to-end is a strong systems answer.
612-Factor apps โ the ones that matter
Twelve-factor is the why behind container/K8s design. The load-bearing factors, compressed:
| Factor | Rule | Why it's load-bearing |
|---|---|---|
| III ยท Config | Config in the environment | Same artifact, different env vars per environment. Never bake config into images; never commit .env. |
| IV ยท Backing services | Attached resources | The DB/queue/LLM-provider is a URL + credentials โ swappable without code change. (A provider-agnostic LLM gateway is this factor, applied to LLMs.) |
| V ยท Build, release, run | Strict separation | Build once, combine with config to make a release, run it. Rollback = redeploy previous release. |
| VI ยท Processes | Stateless | No local session/state; state lives in backing services (Postgres, Redis). The factor that makes horizontal scaling and HPA legal โ any pod serves any request; killing a pod loses nothing. |
| IX ยท Disposability | Fast start, graceful SIGTERM | Assumed by rolling deploys and autoscaling. |
| X ยท Dev/prod parity | Keep envs similar | Containers made this real. |
| XI ยท Logs | Event streams | Structured JSON to stdout; the platform routes it. Never manage log files in-app. |
7Environment & secret management
- Config hierarchy: code defaults โ env-specific config โ env vars/secrets at runtime. Pydantic
BaseSettingsis the idiomatic FastAPI answer โ typed, validated config from env, fails fast at startup on missing values. - Secrets lifecycle: never in git (enforce with pre-commit hooks + CI secret scanning โ gitleaks/trufflehog). A secret once committed is compromised forever via history โ rotate it, don't just delete the line. Runtime delivery: platform secret stores (hardened K8s Secrets, or Vault/cloud managers with dynamic short-lived credentials).
- Rotation must be a non-event โ support two valid credentials simultaneously so you roll without downtime (same pattern as dual API keys).
- Identity over secrets where possible: workload identity / IAM roles / OIDC โ the platform attests "this pod is service X" and mints short-lived credentials; nothing long-lived to leak. The direction the industry is moving; one sentence earns points.
- Don't log secrets: redaction in logging middleware; masked in CI logs; and โ an LLM-era addition you can own โ keep secrets out of prompt payloads and LLM traces. Observability tooling that records full prompts will happily capture a connection string a naive RAG pipeline stuffed into context.
8Infrastructure as Code โ intuition level
The idea: infrastructure (clusters, DBs, queues, DNS, IAM) is declared in versioned files and created/updated by a tool โ instead of console clicking that produces undocumented, unreproducible snowflake environments.
- Declarative + reconciliation, again: Terraform is the same mental loop as K8s โ desired state (HCL) vs actual state (state file + provider APIs) โ
terraform planshows the diff,applyconverges it. If you understand K8s controllers, you already understand Terraform's model; saying that sentence shows synthesis. - Why it wins: review infra changes in PRs; reproduce whole environments (staging = prod's config with smaller numbers); disaster recovery becomes
apply; drift becomes detectable. - Vocabulary: Terraform/OpenTofu (declarative, provider ecosystem), Pulumi/CDK (real languages compiling to the same model), Helm (K8s package templating), GitOps (ArgoCD/Flux: a controller in the cluster pulls desired state from git and reconciles continuously โ git becomes the deploy interface and the audit log;
git revertis a rollback).
You don't need deep Terraform expertise โ "I understand the declarative model well from K8s, I've consumed IaC-managed infra, and I'd be productive in it quickly" is honest and lands.
9Rapid-fire interview Q&A
Walk me through what happens from git push to serving traffic.
Push triggers the workflow โ lint/tests in parallel jobs โ build a SHA-tagged image (layer-cached) โ push to registry โ integration tests against containerized deps โ deploy manifests updated to the new digest (or GitOps repo bump) โ K8s Deployment rolls: new ReplicaSet scales up, readiness probes gate traffic admission via the Service, old pods drain on SIGTERM โ post-deploy smoke checks. Rollback is rollout undo to the previous ReplicaSet.
Container vs VM?
A VM virtualizes hardware and boots a guest kernel; a container is a host process isolated by namespaces/cgroups with a layered filesystem โ millisecond starts, higher density, weaker isolation boundary (shared kernel).
Why is your Dockerfile ordered the way it is?
Cache economics: instructions invalidate top-down, so least-changing layers (base, system deps, Python deps keyed on the lockfile) come first and code COPY comes last โ code edits then rebuild only the final layers, keeping CI builds fast.
Service vs Ingress?
Service = stable internal VIP/DNS load-balancing across ephemeral pods (L4, label-selected). Ingress = the L7 edge โ host/path routing + TLS from outside the cluster into Services, one entry point instead of one cloud LB per service.
How do you deploy a schema change safely?
Expand-migrate-contract: additive migration first (new nullable column/table), deploy code that dual-writes/reads both, backfill, then a later contract migration removes the old shape โ because rolling deploys guarantee old and new code run concurrently against the same DB.
Where do secrets live in your setup?
Never in git or images; CI uses scoped encrypted secrets or OIDC-federated short-lived cloud creds; runtime gets them via the platform secret store into env/files; config is typed and validated at startup (Pydantic settings) so a missing secret fails the deploy, not the 3am request.
10Self-test
Recite the four DORA metrics and the counterintuitive finding.
Deploy frequency, lead time for changes, change failure rate, time-to-restore. Elite teams deploy more often and fail less โ small batches make speed and safety correlated, not a trade-off.
Why "build once, promote everywhere"? What exactly is promoted?
Per-environment rebuilds can silently differ (new base image, new transitive dep). You promote the same image digest from staging to prod; only config varies per environment. Tag with the git SHA so "what's in prod?" has a one-word answer.
Name the three kernel features that make containers possible.
Namespaces (isolated view of PIDs/network/mounts/users), cgroups (CPU/memory limits), union filesystem (layered, copy-on-write). No guest OS โ a container is just a process; shared kernel is the isolation caveat vs VMs.
Why does exec-form CMD matter for K8s graceful shutdown?
Exec-form makes your app PID 1, so it receives SIGTERM directly when the pod terminates. The full chain: removed from Service endpoints โ SIGTERM โ drain in-flight requests โ exit before terminationGracePeriodSeconds โ else SIGKILL.
Which 12-factor rule makes HPA "legal", and why?
VI โ stateless processes. If no request-scoped state lives in the process (state is in Postgres/Redis), any pod can serve any request, so adding/removing replicas is safe. Also required: pods must declare resource requests, because HPA math runs off them.
What's wrong with K8s Secrets by default, and what's the hardened story?
They're base64-encoded, not encrypted. Hardening: etcd encryption at rest, RBAC limiting reads, and/or an external manager (Vault, cloud secret stores) synced via External Secrets Operator. Mounted-file secrets rotate without restarts; env-var secrets don't.
Explain Terraform to someone who knows K8s controllers.
Same model: declared desired state (HCL) vs actual state (state file + provider APIs); plan is the diff, apply is the reconcile step. GitOps (ArgoCD/Flux) closes the loop continuously with git as the deploy interface and audit log.