Architecture¶
This document describes the technical architecture of the GA4GH ServiceInfo service — a lightweight, standalone Go service that standardizes service metadata across GA4GH genomics services.
1. The Problem¶
Every GA4GH service (DRS, TES, WES, TRS) is required to expose a /service-info endpoint. In practice, this metadata is hardcoded inside application code and almost never updated after deployment.
A real example from the GA4GH DRS Starter Kit (version 0.3.2):
{
"version": "0.1.0",
"createdAt": "2020-01-15T12:00:00",
"updatedAt": "2020-01-15T12:00:00",
"environment": "test"
}
The binary version is 0.3.2, but the metadata says 0.1.0. The timestamps are from 2020. The environment says test in a production cluster.
This happens because updating this data requires a code change, a code review, a CI run, and a full container redeployment — for a date field.
2. What the service Does¶
The service is a standalone Go service deployed as its own Pod in Kubernetes. It:
- Serves
GET /service-infowith operator-managed metadata from a YAML ConfigMap. - Validates the metadata against the GA4GH ServiceInfo v1 specification on every load.
- Hot-reloads when the ConfigMap is updated — zero restart, zero downtime.
- Exposes
/healthzand/readyzendpoints for Kubernetes probes. - Never touches the real GA4GH service — no proxying, no coupling, no blast radius.
The Kubernetes Ingress controller (which already exists in every production cluster) routes /service-info requests to the service and everything else directly to the real service.
3. High-Level Architecture¶
graph TD
Client[External Client] -->|HTTP Request /:443| Ingress[Kubernetes Ingress]
Ingress -->|/service-info| SI["ServiceInfo Pod\n(:8080 Go Binary)"]
Ingress -->|All other paths| Backend["GA4GH Genomics Pod\n(DRS / TES / WES / TRS)"]
Config[ConfigMap Volume] -.->|Mounted & watched by fsnotify| SI
Key Properties¶
| Property | Value |
|---|---|
| Language | Go (single static binary) |
| Container image | scratch base, <20MB |
| Memory footprint | ~15–30MB RSS |
| Startup time | <100ms |
| External dependencies | None (Go stdlib net/http) |
| Blast radius | If service crashes, only /service-info returns 503. DRS/TES/WES continue working. |
4. Why Ingress Routing?¶
An earlier design routed 100% of all traffic through the service as a reverse proxy, just to intercept the 1–5% of requests that hit /service-info. That approach had fundamental problems:
| Concern | Reverse Proxy | Ingress Routing (current) |
|---|---|---|
| Blast radius | service crash kills ALL traffic | Only /service-info is affected |
| Traffic overhead | Every request proxied through service | Only metadata requests hit service |
| Footprint | Heavy runtime + dependencies | ~10MB (Go static binary) |
| Integration effort | Restructure entire Pod | Add one Ingress rule |
| Coupling | service + service in same Pod | Completely independent Pods |
The Ingress routing pattern solves a small problem with a small solution.
5. Request Routing (Ingress)¶
The Kubernetes Ingress configuration routes requests based on path:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ga4gh-drs
spec:
rules:
- host: drs.myinstitute.org
http:
paths:
# Service-info → service
- path: /ga4gh/drs/v1/service-info
pathType: Exact
backend:
service:
name: serviceinfo-service
port:
number: 8080
# Everything else → DRS
- path: /
pathType: Prefix
backend:
service:
name: drs
port:
number: 8080
Users add one Ingress rule. That's the entire integration change.
6. Configuration Architecture¶
All metadata lives in a YAML file. In production, this file is mounted from a Kubernetes ConfigMap. For local development, a dummy config file is included in the repository.
The config file path is controlled by the SERVICE_CONFIG_PATH environment variable. If unset, it falls back to ./configs/dummy_service_info.yaml.
Example Configuration¶
id: "org.ga4gh.myinstitute.drs"
name: "My Institute DRS"
type:
group: "org.ga4gh"
artifact: "drs"
version: "1.4.0"
organization:
name: "My Research Institute"
url: "https://myinstitute.org"
version: "1.2.0"
environment: "production"
description: "DRS service for genomic data access."
No operational settings mixed in — the YAML is pure metadata. Deployment topology is handled by Kubernetes manifests.
GA4GH Schema Validation¶
On every load (initial startup and hot-reload), the service validates that the following required fields are present and non-empty:
id,name,versiontype.group,type.artifact,type.versionorganization.name,organization.url
If any required field is missing, the service rejects the configuration and retains the previously loaded valid config. It never crashes or serves invalid metadata.
7. Hot Reload Architecture¶
When a ConfigMap is updated, Kubernetes performs an atomic symlink swap on the mounted directory. The service detects this using fsnotify and reloads configuration atomically:
flowchart TD
A[DevOps edits ConfigMap in Git] --> B[ArgoCD / Flux syncs to Kubernetes]
B --> C[Kubernetes updates ConfigMap<br/>atomic symlink swap]
C --> D[fsnotify goroutine detects change]
D --> E{Is YAML valid?}
E -->|Yes| F[sync.RWMutex updates config in-memory]
E -->|No| G[Log error & retain old config]
F --> H[Next request sees updated values<br/>zero downtime]
G --> H
Implementation Details¶
- Directory watching:
fsnotifywatches the directory containing the config file, not the file itself. This is required because Kubernetes ConfigMap updates work via symlink swaps — the file inode changes, so watching the file directly would miss the update. - Thread safety: A
sync.RWMutexprotects the config pointer. HTTP handlers acquire a read lock (RLock), while the reload goroutine acquires a write lock (Lock) only during the brief pointer swap. - Failure safety: If the new YAML is invalid (missing fields, bad syntax), the reload is rejected with a structured log error. The service continues serving the previous valid configuration.
- Structured logging: All reload events are logged via Go's
log/slogpackage with JSON output, making them compatible with cloud-native observability stacks.
8. Endpoints¶
| Endpoint | Method | Purpose |
|---|---|---|
/service-info |
GET | GA4GH ServiceInfo JSON response |
/healthz |
GET | Kubernetes liveness probe — always returns 200 OK |
/readyz |
GET | Kubernetes readiness probe — returns 200 OK only after config is loaded |
9. Project Structure¶
graph TD
Root["ga4gh_service_info_sidecar_gsoc_2026/"]
Root --> Cmd["cmd/"]
Cmd --> Server["server/"]
Server --> Main["main.go\n(Entrypoint: HTTP server, slog, fsnotify)"]
Root --> Internal["internal/"]
Internal --> Config["config/"]
Config --> ConfigGo["config.go\n(Watcher, fsnotify, RWMutex, validation)"]
Config --> ConfigTest["config_test.go\n(Config load/validation tests)"]
Internal --> Handler["handler/"]
Handler --> SIHandler["service_info.go\n(GET /service-info handler)"]
Handler --> Health["health.go\n(GET /healthz, /readyz handlers)"]
Handler --> HandlerTests["*_test.go\n(Handler unit tests)"]
Internal --> Model["model/"]
Model --> ModelGo["service_info.go\n(GA4GH ServiceInfo Go structs)"]
Root --> Configs["configs/"]
Configs --> DummyYAML["dummy_service_info.yaml\n(Dummy config for local dev)"]
Root --> Docs["docs/\n(MkDocs documentation)"]
Root --> Workflows[".github/workflows/\n(CI: Go vet, test, build)"]
Root --> Integration["integration_test.go\n(E2E integration test suite)"]
10. Phase Roadmap¶
| Phase | Feature | Status |
|---|---|---|
| Phase 0 | Go module, HTTP server, GA4GH JSON response, unit tests | ✅ Done |
| Phase 1 | YAML config loading, GA4GH schema validation, fsnotify hot reload, structured logging (slog) |
✅ Done |
| Phase 2 | Dockerfile, K8s manifests, Ingress rule, Minikube testing | ✅ Done |
| Phase 3 | Helm chart, GitHub Actions CI/CD, quickstart guide | ✅ Done |