Intro
This guide turns REST API CI/CD from abstract theory into an operator-ready workflow. You will inventory what runs today, apply the smallest safe change, verify with observable checks, and roll back when signals fail. The examples use a Node.js Express REST API packaged with Docker and deployed to Kubernetes, but the patterns map to other stacks.
Goals:
- Observe before changing: confirm versions, topology, and state with read-only commands.
- Limit blast radius: change one scoped component per run.
- Keep secrets out of logs: use placeholders and protected variables.
- Verify outcomes: define success and failure signals in advance.
- Make changes reversible: document a tested rollback path.
Assumed targets (adapt for your environment):
- API: Node.js 18+ (any REST framework)
- Container: Docker 24+
- Orchestrator: Kubernetes 1.26+
Version and Environment Inventory
Purpose: capture the current runtime and deployment context without modifying anything.
Prerequisites:
- CLI access to your cluster and registry.
- A health or version endpoint in the API.
- A single authoritative environment naming scheme (for example, dev, staging, prod).
Recommended read-only inventory commands (replace placeholders):
# 1) Timestamp for your ops notes
DATE_UTC=$(date -u +"%Y-%m-%dT%H:%M:%SZ"); echo "$DATE_UTC"
# 2) Local tooling
node --version
npm --version
docker --version
kubectl version --client --output=yaml | sed -n '1,10p'
# 3) Cluster and namespace context
kubectl config current-context
kubectl get ns | grep -E '(^| )<namespace>( |$)'
# 4) Deployed image and replicas
kubectl get deploy <service_name> -n <namespace> -o jsonpath='{.spec.replicas}{"\n"}'
kubectl get deploy <service_name> -n <namespace> -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
# 5) Current rollout state and last changes
kubectl rollout status deploy/<service_name> -n <namespace> --timeout=30s || true
kubectl describe deploy <service_name> -n <namespace> | sed -n '1,50p'
# 6) Application-reported version (read-only)
curl -fsS https://api.<env>.example.com/version | jq -r '.version'
# If an auth token is required:
# curl -fsS -H 'Authorization: Bearer <token_placeholder>' https://api.<env>.example.com/version | jq -r '.version'
Record:
- The deployed image reference and digest (if available).
- The API-reported version.
- The namespace, replicas, and recent events.
Define the expected result and failure signal before changing anything. Example:
- Expected: after deploy, image tag v1.4.3 is running; /version returns 1.4.3; readiness probes stay green; error rate unchanged.
- Failure signals: ImagePullBackOff, CrashLoopBackOff, readiness failures beyond 5 minutes, 5xx rate increase > 2x baseline.
Safe Configuration Path
Objective: perform the smallest justified change with a defined blast radius and a tested rollback path.
Change scope (example): update the API container image to a new tag in one Kubernetes namespace.
Supported versions and prerequisites:
- The pipeline below targets Node.js 18+, Docker Buildx, and GitHub Actions. Adapt to your CI system as needed.
- Secrets (registry credentials, kubeconfig) must be stored in your CI secret store.
Example GitHub Actions pipeline for build, test, scan, and publish:
name: ci
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
tags: [ 'v*.*.*' ]
env:
REGISTRY: <registry.example.com>
IMAGE_NAME: <org>/<service_name>
jobs:
build_test_publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install deps
run: npm ci
- name: Lint and unit tests
run: |
npm run lint
npm test -- --ci --reporters=default --reporters=jest-junit
- name: Compute image tag
id: meta
run: |
SHA=$(git rev-parse --short HEAD)
echo "tag=$SHA" >> $GITHUB_OUTPUT
- name: Docker login
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push image
run: |
docker buildx create --use --name builder || true
docker buildx build \
--platform linux/amd64 \
-t "$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.tag }}" \
-t "$REGISTRY/$IMAGE_NAME:latest" \
--push .
# Optional: add image scanning in your preferred tool
Deployment job (triggered on tag to staging or manual promotion):
deploy_staging:
if: startsWith(github.ref, 'refs/tags/v')
needs: [ build_test_publish ]
runs-on: ubuntu-latest
environment: staging
steps:
- name: Set KUBECONFIG
run: |
mkdir -p $HOME/.kube
echo "$KUBE_CONFIG_BASE64" | base64 -d > $HOME/.kube/config
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64_STAGING }}
- name: Update image (smallest change)
run: |
kubectl -n <namespace> set image deploy/<service_name> \
<container_name>=$REGISTRY/$IMAGE_NAME:${GITHUB_REF_NAME} --record
env:
REGISTRY: <registry.example.com>
IMAGE_NAME: <org>/<service_name>
- name: Wait for rollout
run: kubectl -n <namespace> rollout status deploy/<service_name> --timeout=5m
Blast radius: a single deployment in one namespace. No global config changes.
Rollback path (must be tested):
# If the deployment fails or signals degrade
kubectl -n <namespace> rollout undo deploy/<service_name>
# Or jump to a known good revision
kubectl -n <namespace> rollout undo deploy/<service_name> --to-revision=<n>
If you use Helm, prefer atomic upgrades for built-in rollback:
helm upgrade --install <release_name> <chart_path> \
--namespace <namespace> \
--set image.repository=<registry.example.com>/<org>/<service_name> \
--set image.tag=<version_tag> \
--atomic --timeout 5m
# Roll back if needed
helm rollback <release_name> <revision> --namespace <namespace>
Always keep secrets in the CI secret store. Never echo tokens, keys, or production identifiers in logs.
Verification and Diagnostics
Verification is not a vibe check; it is a scriptable contract. Run these checks after any change:
Smoke checks (golden paths):
# Health
curl -fsS -o /dev/null -w '%{http_code}\n' https://api.<env>.example.com/health
# Readiness (should be 200)
curl -fsS -o /dev/null -w '%{http_code}\n' https://api.<env>.example.com/ready
# Version (should match the deployed tag)
curl -fsS https://api.<env>.example.com/version | jq -r '.version'
Workload health in Kubernetes:
# Pods and conditions
kubectl get pods -n <namespace> -l app=<service_name> -o wide
kubectl describe deploy <service_name> -n <namespace> | sed -n '1,120p'
# Logs for the new pods only (adjust selector)
kubectl logs -n <namespace> -l app=<service_name> --tail=200 --prefix | grep -E 'ERROR|WARN' || true
Canary diagnostics (if you route canary traffic via header):
curl -fsS -H 'X-Canary: 1' https://api.<env>.example.com/version | jq -r '.version'
Database migration check (if your service uses a migration table):
# Example for Postgres via psql; use a read-only connection string from your secret store
# Do NOT paste real credentials
psql '<readonly_conn_string_placeholder>' -c 'select version from schema_migrations order by applied_at desc limit 1;'
Define pass/fail thresholds:
- Pass: health and readiness return 200; version matches target; logs show no new ERROR lines across 5 minutes; pod restarts remain unchanged.
- Fail: sustained 5xx rate increase; readiness flapping; >1 restart per pod within 5 minutes; migration version missing or mismatched.
Failure Modes and Recovery
Common issues and what to do next:
- ImagePullBackOff
- Signal: kubectl events show image pull failures; pods never start.
- Likely causes: wrong tag, registry auth missing.
- Recovery: verify image exists; fix secret; redeploy same tag; if time-critical, roll back:
kubectl -n <namespace> rollout undo deploy/<service_name>
- CrashLoopBackOff
- Signal: containers restart repeatedly.
- Causes: bad env var, incompatible config or migration.
- Recovery: fetch logs for the failing pod, revert to the last good revision:
kubectl -n <namespace> logs deploy/<service_name> --tail=200
kubectl -n <namespace> rollout undo deploy/<service_name>
- Readiness probe failures
- Signal: rollout does not complete within the timeout.
- Causes: cold starts, missing dependency, port mismatch.
- Recovery: roll back if the probe will not stabilize quickly; review probe config; validate dependent services.
- Schema migration failures
- Signal: new code expects a column that does not exist; errors spike on startup.
- Strategy: use expand-contract migrations (additive changes first, removals last). If already impacted, roll back the API and apply a compatible hotfix migration.
- Authorization or secret misconfiguration
- Signal: 401/403 on internal calls; third-party integrations fail.
- Recovery: update the secret in the CI secret store, restart the deployment, and re-run smoke tests. Avoid printing secret values.
- Performance regression
- Signal: latency up, timeouts, CPU throttling.
- Recovery: increase replicas temporarily, scale limits, or roll back while investigating.
Always prefer rolling forward with a minimal hotfix if the failure is well understood and risk is low. Otherwise, roll back within your predefined error budget.
Operations Checklist
Before change:
- Confirm cluster context, namespace, and target workload.
- Capture current image, replicas, and API-reported version.
- Define success metrics and failure thresholds.
- Ensure all secrets and registry credentials are present in the CI secret store.
- If using database migrations, confirm backward compatibility and a tested downgrade or rollback plan.
During change:
- Apply the smallest modification (image tag only, or a single Helm value).
- Wait for rollout completion with a timeout.
- Run smoke checks and compare to the expected signals.
After change:
- Watch logs and pod restarts for at least one full autoscaling window.
- Verify business-critical endpoints (e.g., POST /orders) with a non-sensitive test payload.
- Document the outcome: timestamp, version, who approved, and links to CI runs.
If a failure threshold is breached at any point, execute the rollback command immediately and record the reason.
Conclusion
Reliable REST API CI/CD is a discipline: inventory first, make the smallest safe change, verify with explicit checks, and keep a rollback ready. Use versioned images, protected secrets, and read-only observations to reduce risk. As a next step, pick one low-risk change (for example, update the image tag in staging), run the inventory commands, deploy with the sample pipeline, execute the smoke tests, and practice the rollback once. Repetition builds a workflow that is observable, reversible, and safe to operate at any scale.