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.
DORA metrics โ€” worth citing by name: deploy frequency, lead time for changes, change failure rate, time-to-restore. Elite teams deploy many times daily with lower failure rates. Speed and safety are correlated, not traded off, because small changes are easy to review, test, and roll back.

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).
Migrations must be backward compatible. Old and new code overlap during any gradual rollout, so use expand-migrate-contract: add the new nullable column โ†’ deploy code that dual-writes/reads both โ†’ backfill โ†’ a later contract migration removes the old shape.

Pipeline Runner

Step a commit through the pipeline. Inject a failure and watch where it gets caught โ€” the whole point of stage ordering is catching each class of problem at the cheapest possible stage.

lint โ†’ unit tests โ†’ build image โ†’ scan โ†’ deploy canary 10% โ†’ observe metrics โ†’ promote 100%

Pick a failure mode (or none) and hit Run.

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.
Security pitfall worth knowing: 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.

Layer caching is the thing to demonstrate mastery of. The builder reuses a cached layer iff the instruction and its inputs are unchanged โ€” and invalidation cascades: once one layer changes, every subsequent layer rebuilds. Hence the ordering rule: least-changing first.

Docker layer-cache demo

Same app, two orderings. Hit the button to simulate an ordinary code edit and see which layers the cache throws away (rebuilt vs cached).

Bad: copy code first

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app.main:app"]

idle โ€” no edit yet

Good: deps first

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app"]

idle โ€” no edit yet

COPY . . early means any code edit invalidates the pip-install layer โ€” full reinstall every build.

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?"

Pod

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.

Deployment

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).

Service

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).

Ingress

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.

ConfigMap & Secret

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.

HPA

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.
Graceful shutdown โ€” the story that ties Docker + K8s together: pod termination โ†’ removed from Service endpoints โ†’ SIGTERM to PID 1 (exec-form CMD!) โ†’ app drains in-flight requests โ†’ exits before 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:

FactorRuleWhy it's load-bearing
III ยท ConfigConfig in the environmentSame artifact, different env vars per environment. Never bake config into images; never commit .env.
IV ยท Backing servicesAttached resourcesThe 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, runStrict separationBuild once, combine with config to make a release, run it. Rollback = redeploy previous release.
VI ยท ProcessesStatelessNo 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 ยท DisposabilityFast start, graceful SIGTERMAssumed by rolling deploys and autoscaling.
X ยท Dev/prod parityKeep envs similarContainers made this real.
XI ยท LogsEvent streamsStructured 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 BaseSettings is 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 plan shows the diff, apply converges 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 revert is a rollback).
The one operational warning: Terraform state files contain secrets and coordinate concurrency โ€” remote backend + locking, never local, never committed.

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.

โ† Clean code & patterns LLM fundamentals โ†’