Skip to main content

Kubernetes

This guide shows how to deploy Topaz as a shared, self-managed emulator inside a Kubernetes cluster so multiple applications can use the same instance without each team running their own copy.

Concept

In a microservices setup it quickly becomes wasteful — and hard to keep in sync — for every developer or CI job to maintain a private Topaz instance. A better model is to treat Topaz the same way you treat any shared backing service: deploy it once, give each application team an isolated Azure subscription inside that shared instance, and let Kubernetes DNS take care of the routing.

Topaz Kubernetes architecture — shared emulator with CoreDNS rewrite and per-app subscription isolation

Why CoreDNS instead of extra_hosts?

Docker Compose solves hostname resolution with extra_hosts. In Kubernetes, the equivalent would be adding a hostAliases entry to every Pod spec — which is fragile and doesn't scale.

A cleaner solution is to patch the cluster-wide CoreDNS ConfigMap with a single rewrite rule. Any query for *.topaz.local.dev is transparently redirected to the Topaz ClusterIP service. Application code and SDK clients require no changes — they use the same hostnames they would against real Azure.

Namespace isolation

Each application gets its own Topaz subscription. Resources created inside one subscription are invisible to another, even though both share the same emulator process.

AppTopaz subscriptionResource group
order-service00000000-0000-0000-0000-000000000001rg-order-service
inventory-service00000000-0000-0000-0000-000000000002rg-inventory-service

Pass the subscription ID to each pod as an environment variable:

pod spec
env:
- name: TOPAZ_SUBSCRIPTION_ID
value: "00000000-0000-0000-0000-000000000001"

Kubernetes manifests

Namespaces

namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
name: topaz-system
---
apiVersion: v1
kind: Namespace
metadata:
name: apps

Topaz deployment

Topaz runs as a single-replica Deployment in the topaz-system namespace. The TLS certificate is mounted from a Secret so pods can be recreated without losing it.

topaz-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: topaz
namespace: topaz-system
spec:
replicas: 1
selector:
matchLabels:
app: topaz
template:
metadata:
labels:
app: topaz
spec:
containers:
- name: topaz
image: thecloudtheory/topaz-host:latest
args:
- --certificate-file
- /certs/topaz.crt
- --certificate-key
- /certs/topaz.key
ports:
- containerPort: 8899 # ARM
- containerPort: 8898 # Key Vault
- containerPort: 8891 # Blob Storage, Queue Storage, Table Storage
volumeMounts:
- name: certs
mountPath: /certs
readOnly: true
- name: data
mountPath: /app/.topaz
volumes:
- name: certs
secret:
secretName: topaz-tls
- name: data
persistentVolumeClaim:
claimName: topaz-data

Topaz service

A ClusterIP service makes Topaz reachable cluster-wide as topaz.topaz-system.svc.cluster.local. The service exposes all emulated ports.

topaz-service.yaml
apiVersion: v1
kind: Service
metadata:
name: topaz
namespace: topaz-system
spec:
selector:
app: topaz
ports:
- name: arm
port: 8899
targetPort: 8899
- name: keyvault
port: 8898
targetPort: 8898
- name: storage
port: 8891
targetPort: 8891 # Blob, Queue, and Table Storage all share this port

TLS secret

Create the secret from the Topaz certificate files before deploying:

create-tls-secret.sh
kubectl create secret generic topaz-tls \
--from-file=topaz.crt=certificate/topaz.crt \
--from-file=topaz.key=certificate/topaz.key \
-n topaz-system \
--dry-run=client -o yaml | kubectl apply -f -

The certificate files are available in the certificate/ directory of the Topaz repository.

CoreDNS rewrite

Patch the coredns ConfigMap in kube-system to add two rewrite rules — one exact match for the ARM control-plane host and one regex rule for all data-plane SDK hostnames:

Corefile rules
rewrite name exact topaz.local.dev topaz.topaz-system.svc.cluster.local
rewrite name regex (.+)\.topaz\.local\.dev topaz.topaz-system.svc.cluster.local answer auto

The full patch script is shown below. It is idempotent — safe to run multiple times.

patch-coredns.sh
MARKER="topaz.local.dev"
REWRITE_RULE=" rewrite name exact topaz.local.dev topaz.topaz-system.svc.cluster.local\n rewrite name regex (.+)\\.topaz\\.local\\.dev topaz.topaz-system.svc.cluster.local answer auto"

CURRENT=$(kubectl get configmap coredns -n kube-system -o jsonpath='{.data.Corefile}')

if echo "$CURRENT" | grep -q "$MARKER"; then
echo "Rewrite rule already present."
exit 0
fi

PATCHED=$(echo "$CURRENT" | awk "/forward \\./{print \"$REWRITE_RULE\"}1")

kubectl patch configmap coredns -n kube-system \
--type=merge \
--patch "{\"data\":{\"Corefile\":$(printf '%s' "$PATCHED" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}}"

kubectl rollout restart deployment/coredns -n kube-system
Two rules are required

The ARM client (TopazArmClientOptions) connects to https://topaz.local.dev:8899. This is the bare hostname — not a subdomain — so the (.+)\.topaz\.local\.dev regex does not match it. The exact rule covers this case.

Application code

Applications connect to Topaz exactly as they would in any other environment. The only Kubernetes-specific step is reading the subscription ID from an environment variable.

Startup.cs
var subscriptionId = Environment.GetEnvironmentVariable("TOPAZ_SUBSCRIPTION_ID")
?? throw new InvalidOperationException("TOPAZ_SUBSCRIPTION_ID is required");

var credential = new AzureLocalCredential(Globals.GlobalAdminId);

// Wait for Topaz to be ready, then register the subscription.
using var topazClient = new TopazArmClient(credential);
for (var attempt = 1; ; attempt++)
{
if (await topazClient.CheckIfReadyAsync()) break;
if (attempt >= 30) throw new TimeoutException("Topaz did not become ready.");
await Task.Delay(TimeSpan.FromSeconds(2));
}
await topazClient.CreateSubscriptionAsync(Guid.Parse(subscriptionId), "my-app-sub");

// Use standard ARM + data-plane SDK clients — no endpoint overrides needed.
var armClient = new ArmClient(credential, subscriptionId, TopazArmClientOptions.New);

Because CoreDNS rewrites the hostnames, TopazArmClientOptions.New and TopazResourceHelpers.GetAzureStorageConnectionString(...) work without modification.

Trusting the Topaz certificate in Docker images

Application images must trust the Topaz certificate so that Azure SDK TLS connections succeed. Add the following to the app's Dockerfile:

Dockerfile
COPY topaz.crt /usr/local/share/ca-certificates/topaz.crt
RUN update-ca-certificates

Copy the certificate into the Docker build context before building:

build image
cp certificate/topaz.crt apps/my-service/topaz.crt
docker build -t my-service:latest apps/my-service/

Complete example

A fully runnable example using k3d (k3s in Docker) with two sample services is available in the repository:

Examples/Topaz.Example.Kubernetes/

The example includes:

  • setup.sh — one-command cluster provisioning
  • k8s/ — all Kubernetes manifests and the CoreDNS patch script
  • apps/order-service/ — Blob Storage-backed orders API
  • apps/inventory-service/ — Queue Storage-backed inventory events API
Star on GitHub