Skip to content

Kubernetes Deployment

Deploy Rift in Kubernetes for production mock services and chaos engineering.


Quick Start

Basic Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rift
  labels:
    app: rift
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rift
  template:
    metadata:
      labels:
        app: rift
    spec:
      containers:
        - name: rift
          image: zainalpour/rift-proxy:latest
          ports:
            - name: admin
              containerPort: 2525
            - name: metrics
              containerPort: 9090
          env:
            - name: MB_PORT
              value: "2525"
            - name: MB_ALLOW_INJECTION
              value: "true"
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
          livenessProbe:
            httpGet:
              path: /health
              port: admin
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: admin
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: rift
spec:
  selector:
    app: rift
  ports:
    - name: admin
      port: 2525
      targetPort: admin
    - name: metrics
      port: 9090
      targetPort: metrics

Probes and --api-key

With MB_APIKEY / --api-key set, every admin API path — /health included — answers 401 without the key, so a plain httpGet probe on the admin port fails. Use rift healthcheck as an exec probe instead: it presents the key the container already holds from MB_APIKEY (issue

1154), so the secret stays in the pod's environment rather than being copied into the manifest the

way an httpGet httpHeaders entry would put it:

          livenessProbe:
            exec:
              command: ["rift", "healthcheck"]
            timeoutSeconds: 3
          readinessProbe:
            exec:
              command: ["rift", "healthcheck"]
            timeoutSeconds: 3

Set timeoutSeconds explicitly: an exec probe defaults to 1s, while rift healthcheck waits up to 2s by default, so without it a slow /health is killed by the kubelet instead of being reported by the probe. The key must come from the pod's environment (MB_APIKEY, e.g. from a Secret) — the probe is a separate process and does not see the container's args.

The image's own Docker HEALTHCHECK is ignored by Kubernetes, so declare the probe explicitly as above. Probing the metrics port with httpGet still works too, but it only proves the metrics listener is up, not that the admin API is healthy.

Shutdown

A pod stops promptly: rift handles the SIGTERM the kubelet sends, including as the container's PID 1 (issue #1155), so the default terminationGracePeriodSeconds is ample — the server's own shutdown is bounded at about three seconds. It exits 0 and leaves any --datadir state on its volume. No init process and no shortened grace period are needed.

IPv6 and dual-stack clusters

No flag is needed. With no --host (and no MB_HOST), the admin API and /metrics listen on every interface, IPv6 and IPv4 ([::] dual-stack, falling back to 0.0.0.0 on a node without IPv6), as Mountebank does. On an IPv6 single-stack or dual-stack cluster the kubelet's httpGet probes reach the pod's IPv6 address and Prometheus can scrape /metrics, so the manifests above work as written.

Setting --host 0.0.0.0 (or MB_HOST=0.0.0.0) explicitly makes the pod IPv4-only: an explicit --host binds exactly that address. Images before this change set MB_HOST=0.0.0.0 themselves; unset it if you pin one of them on an IPv6 cluster.

A Service on an IPv6 cluster needs nothing from Rift. ipFamilyPolicy and ipFamilies matter only when you want a dual-stack Service; see the Kubernetes IPv4/IPv6 dual-stack docs.


Configuration with ConfigMap

ConfigMap for Imposters

apiVersion: v1
kind: ConfigMap
metadata:
  name: rift-imposters
data:
  imposters.json: |
    {
      "imposters": [
        {
          "port": 4545,
          "protocol": "http",
          "name": "User Service Mock",
          "stubs": [
            {
              "predicates": [{ "equals": { "path": "/health" } }],
              "responses": [{ "is": { "statusCode": 200, "body": "OK" } }]
            },
            {
              "predicates": [{ "equals": { "path": "/users" } }],
              "responses": [{
                "is": {
                  "statusCode": 200,
                  "headers": { "Content-Type": "application/json" },
                  "body": [{ "id": 1, "name": "Test User" }]
                }
              }]
            }
          ]
        }
      ]
    }

Deployment with ConfigMap

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rift
spec:
  template:
    spec:
      containers:
        - name: rift
          image: zainalpour/rift-proxy:latest
          args: ["--configfile", "/config/imposters.json"]
          ports:
            - name: admin
              containerPort: 2525
            - name: imposter
              containerPort: 4545
          volumeMounts:
            - name: config
              mountPath: /config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: rift-imposters

TLS Configuration

TLS Secret

apiVersion: v1
kind: Secret
metadata:
  name: rift-tls
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-cert>
  tls.key: <base64-encoded-key>

HTTPS Imposter

<%- stringify('…') %> inlines a file's contents into the JSON string when the config is loaded; an absolute path is used as-is, a relative one is resolved against the config file's directory.

apiVersion: v1
kind: ConfigMap
metadata:
  name: rift-https-config
data:
  imposters.json: |
    {
      "imposters": [{
        "port": 4545,
        "protocol": "https",
        "key": "<%- stringify('/tls/tls.key') %>",
        "cert": "<%- stringify('/tls/tls.crt') %>",
        "stubs": [...]
      }]
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rift
spec:
  template:
    spec:
      containers:
        - name: rift
          image: zainalpour/rift-proxy:latest
          args: ["--configfile", "/config/imposters.json"]
          volumeMounts:
            - name: config
              mountPath: /config
            - name: tls
              mountPath: /tls
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: rift-https-config
        - name: tls
          secret:
            secretName: rift-tls

Sidecar Pattern

Application with Rift Sidecar

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
        # Main application
        - name: app
          image: my-app:latest
          env:
            - name: EXTERNAL_API_URL
              value: "http://localhost:4545"

        # Rift sidecar
        - name: rift
          image: zainalpour/rift-proxy:latest
          args: ["--configfile", "/config/imposters.json"]
          ports:
            - containerPort: 4545
          volumeMounts:
            - name: mock-config
              mountPath: /config

      volumes:
        - name: mock-config
          configMap:
            name: my-app-mocks

Intercept listener as a sidecar

To mock an external HTTPS host the app hard-codes, run the intercept listener in the sidecar. Containers in a pod share a network namespace, so the app reaches it on 127.0.0.1. Keep the CA pair in a Secret (made with rift intercept-ca generate --out-dir ca, see intercept-ca). Rift reads it from a mounted volume through the config block's caCertPath / caKeyPath, and the app trusts the certificate from its own mount. Do not also set RIFT_INTERCEPT_* env vars on the sidecar: with an intercept block that is a startup error.

apiVersion: v1
kind: Secret
metadata:
  name: rift-intercept-ca
stringData:
  ca-cert.pem: |
    -----BEGIN CERTIFICATE-----
    ...
  ca-key.pem: |
    -----BEGIN PRIVATE KEY-----
    ...
---
# in the Deployment's pod spec
securityContext:
  fsGroup: 1000                  # rift runs as uid 1000; lets it read the 0440 key
containers:
  - name: app
    image: my-app:latest
    env:
      - name: HTTPS_PROXY
        value: http://127.0.0.1:8080
      - name: NO_PROXY
        value: localhost,127.0.0.1,.svc,.cluster.local   # in-cluster hosts bypass the proxy
      - name: SSL_CERT_FILE
        value: /certs/ca-cert.pem
    volumeMounts:
      - name: ca
        mountPath: /certs
        readOnly: true
  - name: rift
    image: zainalpour/rift-proxy:latest
    args: ["--configfile", "/config/mock.json"]   # holds the `intercept` block and its rules
    volumeMounts:
      - name: mock-config
        mountPath: /config
      - name: ca-pair
        mountPath: /ca
        readOnly: true
volumes:
  - name: ca
    secret:
      secretName: rift-intercept-ca
      items: [{ key: ca-cert.pem, path: ca-cert.pem }]   # the app never gets the key
  - name: ca-pair
    secret:
      secretName: rift-intercept-ca
      defaultMode: 0440
      items:
        - { key: ca-cert.pem, path: ca-cert.pem }
        - { key: ca-key.pem, path: ca-key.pem }
  - name: mock-config
    configMap:
      name: my-app-mocks

The intercept block in mock.json is the one shown under Docker, with "caCertPath": "/ca/ca-cert.pem" and "caKeyPath": "/ca/ca-key.pem"; the listener can bind 127.0.0.1 here since only the pod's own app connects. Other trust variables (NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, a JVM truststore) and which of them replace the default roots: Trusting the CA from the SUT. A forward rule reaches the sidecar's own 127.0.0.1 unless it names a host (details).

Each replica has its own listener and its own rules, like everything else in a replicated Rift (see High Availability). Editing the ConfigMap and calling POST /admin/reload re-applies the rules; the listener itself is boot-only.


High Availability

Multi-Replica Deployment

Each replica is an independent Rift: an imposter created through the admin API exists only on the replica that received the request, and so do recorded requests. Behind a Service, admin calls and verification land on arbitrary pods. Replicate only when every pod loads the same imposters at startup (--configfile from a ConfigMap) and nothing mutates them at runtime. Replicas do not share flow state either — see Shared state across pods.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rift
spec:
  replicas: 3
  template:
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: rift
                topologyKey: kubernetes.io/hostname
      containers:
        - name: rift
          image: zainalpour/rift-proxy:latest

Shared state across pods

The engine ships the in-memory flow store only (the Redis backend was removed in 0.22.0), so scenario state, ctx.state / stateOps values and their TTLs are per pod: N pods behind a Service are N independent state machines, and stub cycling, proxy recordings and the request journal are per pod too. If you need state shared across processes, run rift-cluster instead of scaling this engine horizontally. Until then, keep stateful mocks on a single replica.

Horizontal Pod Autoscaler

The same caveat applies: a pod the autoscaler adds starts with only the startup config and none of the existing flow state. Autoscale only stateless mocks.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: rift
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: rift
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Monitoring

ServiceMonitor for Prometheus

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: rift
spec:
  selector:
    matchLabels:
      app: rift
  endpoints:
    - port: metrics
      interval: 15s
      path: /metrics

PodMonitor

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: rift
spec:
  selector:
    matchLabels:
      app: rift
  podMetricsEndpoints:
    - port: metrics
      interval: 15s

Namespace Isolation

Dedicated Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: test-mocks
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rift
  namespace: test-mocks
spec:
  # ... deployment spec

Network Policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: rift-policy
  namespace: test-mocks
spec:
  podSelector:
    matchLabels:
      app: rift
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: test-runners
      ports:
        - port: 2525
        - port: 4545

Helm Chart (Example)

Rift does not publish a Helm chart. The values below are a starting point for a chart of your own; map config.allowInjection to MB_ALLOW_INJECTION, config.logLevel to MB_LOGLEVEL, and imposters to a ConfigMap passed with --configfile.

values.yaml

replicaCount: 1

image:
  repository: zainalpour/rift-proxy
  tag: latest
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  adminPort: 2525
  metricsPort: 9090

resources:
  limits:
    cpu: 1000m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 128Mi

config:
  allowInjection: true
  logLevel: info

imposters: |
  {
    "imposters": []
  }

Troubleshooting

Check Pod Status

kubectl get pods -l app=rift
kubectl describe pod -l app=rift
kubectl logs -l app=rift

Port Forward for Testing

kubectl port-forward svc/rift 2525:2525
curl http://localhost:2525/imposters

Debug Container

kubectl exec -it deployment/rift -- /bin/sh

The default image has a shell but no curl; the -static image has neither. For the static image, use an ephemeral debug container instead:

kubectl debug -it <rift-pod-name> --image=busybox --target=rift