Shams
Back to blog

Building bc-status: A Real-Time Deployment Dashboard

·6 min read·Shams K

Every engineering team eventually hits the same friction: "Is this deployed? Which version is running? Is it healthy?" — questions scattered across kubectl commands, GitHub Actions tabs, and Slack messages.

I built bc-status to answer all of them in one place.

The best deployment tool is the one your team actually opens instead of running kubectl.

What It Does

bc-status is an internal deployment dashboard that gives our team real-time visibility into every service across staging and production. At a glance, you can see:

  • Deployed versions — what image tag is running in each environment
  • Health status — live health checks with up/down indicators
  • Rollout state — whether a deployment is stable or mid-rollout
  • Latest builds — newest available image from the registry
  • GitHub CI status — latest workflow runs and commit history
  • One-click promotion — deploy staging builds to production directly from the UI

The Tech Stack

LayerTechnologyPurpose
FrontendReact, TypeScript, RTK QueryDashboard UI, real-time polling
BackendPython, FastAPIAPI aggregation, deployment orchestration
InfrastructureKubernetes, NginxService discovery, reverse proxy
IntegrationsGitHub API, Nexus Registry, Minio S3CI/CD, images, artifacts

Architecture

Loading diagram...

The frontend polls the backend at configurable intervals. The backend aggregates data from four external systems in parallel using asyncio.gather(), keeping response times low even when querying multiple clusters and registries.

Request Flow

  1. Frontend sends GET /api/services/{key}/status
  2. Nginx strips /api prefix, proxies to FastAPI on port 8000
  3. Backend fires concurrent requests to Kubernetes, Nexus, GitHub, and Minio
  4. Responses are aggregated into a single JSON payload
  5. Frontend updates the UI with version diffs, health dots, and rollout status

Two Types of Services

We track two kinds of services, each with different deployment and monitoring strategies:

API Services (Kubernetes-deployed)

These are backend services running as Kubernetes Deployments. bc-status reads their state directly from the K8s API:

async def get_deployment_version(self, name: str, namespace: str) -> str:
    deployment = await asyncio.to_thread(
        self.apps_v1.read_namespaced_deployment, name, namespace
    )
    image = deployment.spec.template.spec.containers[0].image
    return image.split(":")[-1] if ":" in image else "latest"

Health checks hit each service's /health endpoint with a 5-second timeout. The rollout status compares ready replicas to desired replicas — if they differ, a deployment is in progress.

UI Services (Minio-hosted)

Frontend apps are built as static artifacts, stored in Minio (S3-compatible storage), and served via Cloudflare. Their version comes from a version.txt file in the bucket:

async def get_ui_version(self, bucket: str, prefix: str) -> str:
    version_path = f"{prefix}/version.txt"
    response = await asyncio.to_thread(
        self.client.get_object, bucket, version_path
    )
    return response.read().decode().strip()

The Deployment Promotion System

The most impactful feature is one-click production promotion. Instead of manually editing Helm values and waiting for ArgoCD, engineers pick a version from a dropdown and hit deploy.

For API Services

Promotion triggers a GitHub Actions workflow that updates the image tag in our GitOps repository:

Loading diagram...

The backend dispatches a promote-prod.yml workflow with the selected image tag. GitHub Actions updates the Helm values file in our bc-k8s repository, and ArgoCD automatically syncs the change to the cluster.

For UI Services

UI promotion is even simpler — the backend copies the artifact directly between Minio buckets:

async def deploy_artifact(self, repo: str, version: str,
                          source_env: str, target_env: str):
    # Download artifact from staging
    artifact = f"{repo}/{source_env}/{version}.tar.gz"
    data = self.client.get_object("artifacts", artifact)
 
    # Extract and upload to production bucket
    with tarfile.open(fileobj=io.BytesIO(data.read())) as tar:
        for member in tar.getmembers():
            target_path = f"{repo}/{target_env}/{member.name}"
            self.client.put_object("frontend", target_path, ...)

Smart Polling

The dashboard uses an adaptive polling strategy to balance freshness with efficiency:

ContextIntervalReason
Dashboard health60sLow-frequency overview
Service detail30sActive monitoring
During deployment5sRollout tracking

The frontend detects active deployments via the rollout status field. When a service is mid-rollout (ready replicas != desired replicas), polling speeds up to 5 seconds so engineers can watch the deployment progress in real-time. A countdown timer in the header shows when the next refresh will happen.

Service Configuration

Adding a new service requires zero code changes — just append to services.json:

{
    "key": "bc-invoice-api",
    "name": "Invoice API",
    "type": "api",
    "github_repo": "bc-invoice-api",
    "branch": "main",
    "stg": {
        "health_url": "https://invoice-stg.bytecode.ca/api/health"
    },
    "prod": {
        "health_url": "https://invoice.bytecode.ca/api/health"
    }
}

The backend reads this on startup and knows how to discover, monitor, and deploy each service based on its type. No restart needed after adding services — just redeploy.

Advanced Debugging Features

Beyond basic status monitoring, bc-status includes tools that save engineers from jumping into kubectl:

  • Environment Variables — side-by-side diff of STG vs PROD env vars, ConfigMaps, and Secrets
  • Volume Mounts — inspect PVCs, emptyDir, secrets, and configmaps mounted to pods
  • Route Paths — visualize the full network path from Pod to K8s Service to Cloudflare Tunnel to public URL
  • Pod Details — see which pods are running, their names, and rollout state

Key Design Decisions

Why FastAPI over NestJS/Express?

The backend is pure aggregation — no business logic, no database, no auth. Python's asyncio with httpx makes concurrent HTTP calls trivial, and the Kubernetes Python client is the most mature K8s SDK available. FastAPI gave us automatic OpenAPI docs for free.

Why RTK Query?

RTK Query's built-in polling, caching, and cache invalidation made the adaptive polling strategy simple to implement. When a deployment is detected, we just change the pollingInterval — RTK Query handles the rest.

Why not a WebSocket approach?

Push-based updates would reduce latency, but the external systems we query (K8s API, Nexus, GitHub) don't offer webhooks for all the data we need. Polling is simpler, more reliable, and good enough for 5-second refresh intervals.


Closing Thoughts

bc-status turned deployment visibility from a multi-tool scavenger hunt into a single browser tab. The one-click promotion feature alone saved us from dozens of manual GitOps commits per week.

The key takeaway: internal tools don't need to be complex to be valuable. A focused dashboard that answers three questions — what's deployed, is it healthy, can I promote it — eliminated an entire class of deployment friction for the team.

Build the tool you wish you had, then make it one click simpler.