Skip to content

Service Discovery Stack

Overview

The Service Discovery Stack provides service mesh capabilities, secrets management, and service registry for the HealthFlow NDP platform using HashiCorp Consul and Vault.

Stack Architecture

Components

1. HashiCorp Consul

Purpose: Service discovery, health checking, and service mesh

Version: Consul 1.18.x

Features:

  • Service Discovery: Automatic service registration and DNS-based discovery
  • Health Checking: Built-in health checks for services
  • KV Store: Distributed key-value store for configuration
  • Service Mesh: Secure service-to-service communication with Consul Connect
  • Multi-datacenter: Support for federation across data centers

Use Cases:

  • Service registration and discovery
  • Configuration management
  • Distributed locks
  • Service segmentation
  • Health monitoring

2. HashiCorp Vault

Purpose: Secrets management and data protection

Version: Vault 1.16.x

Features:

  • Dynamic Secrets: Generate secrets on-demand with automatic rotation
  • Data Encryption: Encrypt data in transit and at rest
  • Leasing & Renewal: All secrets have a lease with automatic renewal
  • Revocation: Secrets can be revoked immediately
  • Audit Logging: Detailed audit trail of all operations
  • High Availability: Support for HA deployment with Consul backend

Secrets Managed:

  • Database credentials (PostgreSQL, MySQL, MongoDB, Redis)
  • API keys and tokens
  • TLS certificates
  • Encryption keys
  • Cloud provider credentials
  • Third-party service credentials (EDA, MOH, Insurance)

3. Consul Agents

Purpose: Local service mesh proxy and health checking

Deployment: DaemonSet (runs on all nodes)

Features:

  • Local DNS interface for service discovery
  • Health check execution
  • Service registration
  • Sidecar proxy for service mesh

Kubernetes Manifests

Namespace

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: discovery-stack
  labels:
    name: discovery-stack
    stack: infrastructure

Consul StatefulSet

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: consul
  namespace: discovery-stack
spec:
  serviceName: consul
  replicas: 3
  selector:
    matchLabels:
      app: consul
  template:
    metadata:
      labels:
        app: consul
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - consul
            topologyKey: kubernetes.io/hostname
      containers:
      - name: consul
        image: hashicorp/consul:1.18
        args:
          - "agent"
          - "-server"
          - "-bootstrap-expect=3"
          - "-ui"
          - "-data-dir=/consul/data"
          - "-bind=0.0.0.0"
          - "-client=0.0.0.0"
          - "-advertise=$(POD_IP)"
          - "-retry-join=consul-0.consul.discovery-stack.svc.cluster.local"
          - "-retry-join=consul-1.consul.discovery-stack.svc.cluster.local"
          - "-retry-join=consul-2.consul.discovery-stack.svc.cluster.local"
          - "-domain=cluster.local"
          - "-datacenter=healthflow-dc1"
          - "-disable-host-node-id"
        env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        ports:
        - containerPort: 8500
          name: http
        - containerPort: 8600
          name: dns
        - containerPort: 8300
          name: server
        - containerPort: 8301
          name: serf-lan
        - containerPort: 8302
          name: serf-wan
        volumeMounts:
        - name: consul-data
          mountPath: /consul/data
        resources:
          limits:
            cpu: "1"
            memory: 2Gi
          requests:
            cpu: "500m"
            memory: 1Gi
        livenessProbe:
          httpGet:
            path: /v1/status/leader
            port: 8500
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /v1/status/peers
            port: 8500
          initialDelaySeconds: 10
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: consul-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3
      resources:
        requests:
          storage: 20Gi

Consul Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: consul
  namespace: discovery-stack
  labels:
    app: consul
spec:
  type: ClusterIP
  clusterIP: None
  selector:
    app: consul
  ports:
  - name: http
    port: 8500
    targetPort: 8500
  - name: dns
    port: 8600
    targetPort: 8600
  - name: server
    port: 8300
    targetPort: 8300
  - name: serf-lan-tcp
    port: 8301
    targetPort: 8301
    protocol: TCP
  - name: serf-lan-udp
    port: 8301
    targetPort: 8301
    protocol: UDP

Consul UI Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: consul-ui
  namespace: discovery-stack
spec:
  type: ClusterIP
  selector:
    app: consul
  ports:
  - name: http
    port: 8500
    targetPort: 8500

Vault StatefulSet

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: vault
  namespace: discovery-stack
spec:
  serviceName: vault
  replicas: 3
  selector:
    matchLabels:
      app: vault
  template:
    metadata:
      labels:
        app: vault
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - vault
            topologyKey: kubernetes.io/hostname
      containers:
      - name: vault
        image: hashicorp/vault:1.16
        args:
          - "server"
        env:
        - name: VAULT_ADDR
          value: "http://127.0.0.1:8200"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: VAULT_API_ADDR
          value: "http://$(POD_IP):8200"
        - name: VAULT_CLUSTER_ADDR
          value: "https://$(POD_IP):8201"
        ports:
        - containerPort: 8200
          name: http
        - containerPort: 8201
          name: internal
        volumeMounts:
        - name: config
          mountPath: /vault/config
        - name: vault-data
          mountPath: /vault/data
        resources:
          limits:
            cpu: "1"
            memory: 1Gi
          requests:
            cpu: "500m"
            memory: 512Mi
        securityContext:
          capabilities:
            add:
            - IPC_LOCK
        livenessProbe:
          httpGet:
            path: /v1/sys/health?standbyok=true
            port: 8200
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204
            port: 8200
          initialDelaySeconds: 10
          periodSeconds: 5
      volumes:
      - name: config
        configMap:
          name: vault-config
  volumeClaimTemplates:
  - metadata:
      name: vault-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3
      resources:
        requests:
          storage: 10Gi

Vault ConfigMap

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: vault-config
  namespace: discovery-stack
data:
  vault.hcl: |
    ui = true
    
    storage "consul" {
      address = "consul:8500"
      path    = "vault/"
    }
    
    listener "tcp" {
      address     = "0.0.0.0:8200"
      tls_disable = 1
    }
    
    api_addr = "http://POD_IP:8200"
    cluster_addr = "https://POD_IP:8201"
    
    disable_mlock = false

Vault Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: vault
  namespace: discovery-stack
spec:
  type: ClusterIP
  selector:
    app: vault
  ports:
  - name: http
    port: 8200
    targetPort: 8200
  - name: internal
    port: 8201
    targetPort: 8201

Service Dependencies

Deployment Instructions

1. Create Namespace

bash
kubectl create namespace discovery-stack

2. Deploy Consul

bash
# Deploy Consul StatefulSet
kubectl apply -f consul/statefulset.yaml

# Deploy Consul Services
kubectl apply -f consul/service.yaml
kubectl apply -f consul/service-ui.yaml

# Wait for Consul to be ready
kubectl wait --for=condition=ready pod -l app=consul -n discovery-stack --timeout=300s

3. Verify Consul

bash
# Check Consul pods
kubectl get pods -n discovery-stack -l app=consul

# Check Consul cluster members
kubectl exec -n discovery-stack consul-0 -- consul members

# Access Consul UI (port-forward)
kubectl port-forward -n discovery-stack svc/consul-ui 8500:8500
# Visit http://localhost:8500

4. Deploy Vault

bash
# Create Vault config
kubectl apply -f vault/configmap.yaml

# Deploy Vault StatefulSet
kubectl apply -f vault/statefulset.yaml

# Deploy Vault Service
kubectl apply -f vault/service.yaml

# Wait for Vault pods
kubectl wait --for=condition=ready pod -l app=vault -n discovery-stack --timeout=300s

5. Initialize Vault

bash
# Initialize Vault (run once)
kubectl exec -n discovery-stack vault-0 -- vault operator init -format=json > vault-keys.json

# IMPORTANT: Save vault-keys.json securely! It contains unseal keys and root token

# Extract unseal keys
UNSEAL_KEY_1=$(cat vault-keys.json | jq -r '.unseal_keys_b64[0]')
UNSEAL_KEY_2=$(cat vault-keys.json | jq -r '.unseal_keys_b64[1]')
UNSEAL_KEY_3=$(cat vault-keys.json | jq -r '.unseal_keys_b64[2]')
ROOT_TOKEN=$(cat vault-keys.json | jq -r '.root_token')

# Unseal Vault on all pods
for pod in vault-0 vault-1 vault-2; do
  kubectl exec -n discovery-stack $pod -- vault operator unseal $UNSEAL_KEY_1
  kubectl exec -n discovery-stack $pod -- vault operator unseal $UNSEAL_KEY_2
  kubectl exec -n discovery-stack $pod -- vault operator unseal $UNSEAL_KEY_3
done

# Login to Vault
kubectl exec -n discovery-stack vault-0 -- vault login $ROOT_TOKEN

6. Configure Vault

bash
# Enable secrets engines
kubectl exec -n discovery-stack vault-0 -- vault secrets enable -path=secret kv-v2
kubectl exec -n discovery-stack vault-0 -- vault secrets enable database

# Enable authentication methods
kubectl exec -n discovery-stack vault-0 -- vault auth enable kubernetes

# Configure Kubernetes auth
kubectl exec -n discovery-stack vault-0 -- vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc.cluster.local:443"

Vault Secrets Configuration

Database Secrets Engine

bash
# Configure PostgreSQL dynamic secrets
kubectl exec -n discovery-stack vault-0 -- vault write database/config/postgresql \
  plugin_name=postgresql-database-plugin \
  allowed_roles="ndp-app" \
  connection_url="postgresql://{{username}}:{{password}}@postgresql.data-stack:5432/ndp?sslmode=disable" \
  username="vault" \
  password="vault-password"

# Create role for dynamic credentials
kubectl exec -n discovery-stack vault-0 -- vault write database/roles/ndp-app \
  db_name=postgresql \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

Static Secrets

bash
# Store static secrets
kubectl exec -n discovery-stack vault-0 -- vault kv put secret/ndp/api \
  eda_api_key="eda-secret-key" \
  moh_api_key="moh-secret-key"

# Store Redis password
kubectl exec -n discovery-stack vault-0 -- vault kv put secret/ndp/redis \
  password="redis-password"

Application Access Policy

bash
# Create policy for NDP applications
kubectl exec -n discovery-stack vault-0 -- vault policy write ndp-app - <<EOF
# Read application secrets
path "secret/data/ndp/*" {
  capabilities = ["read", "list"]
}

# Generate database credentials
path "database/creds/ndp-app" {
  capabilities = ["read"]
}
EOF

# Bind policy to Kubernetes service account
kubectl exec -n discovery-stack vault-0 -- vault write auth/kubernetes/role/ndp-app \
  bound_service_account_names=prescription-service,dispense-service \
  bound_service_account_namespaces=applications \
  policies=ndp-app \
  ttl=1h

Consul Service Registration

Manual Service Registration

bash
# Register a service in Consul
kubectl exec -n discovery-stack consul-0 -- consul services register - <<EOF
{
  "Name": "postgresql",
  "Address": "postgresql.data-stack.svc.cluster.local",
  "Port": 5432,
  "Tags": ["database", "postgres"],
  "Check": {
    "TCP": "postgresql.data-stack.svc.cluster.local:5432",
    "Interval": "10s"
  }
}
EOF

Automatic Service Registration

Applications can register themselves using Consul's Kubernetes sync:

yaml
apiVersion: v1
kind: Service
metadata:
  name: prescription-service
  namespace: applications
  annotations:
    "consul.hashicorp.com/service-name": "prescription-service"
    "consul.hashicorp.com/service-tags": "ndp,api,v1"
    "consul.hashicorp.com/service-port": "8080"
spec:
  selector:
    app: prescription-service
  ports:
  - port: 8080
    targetPort: 8080

Resource Requirements

Estimates

These are rough estimates for development/staging. Production should be scaled based on load.

ServiceCPUMemoryStorageReplicasNotes
Consul0.5 core1 GB20 GB3HA cluster
Vault0.5 core512 MB10 GB3HA cluster
Consul Agent0.1 core128 MB-Per nodeDaemonSet

Best Practices

Vault Security

  1. Never commit unseal keys to Git
  2. Use auto-unseal in production (Hardware HSM, Transit secrets engine, or local key management)
  3. Enable audit logging
  4. Rotate root token regularly
  5. Use namespaces for multi-tenancy
  6. Enable MFA for sensitive operations

Consul Configuration

  1. Enable ACLs in production
  2. Use TLS for all communication
  3. Implement intentions for service mesh security
  4. Monitor cluster health
  5. Backup Consul data regularly

Secret Management

  1. Use dynamic secrets whenever possible
  2. Set appropriate TTL for leases
  3. Implement secret rotation
  4. Use Vault agent for secret injection
  5. Audit secret access

Troubleshooting

Vault is Sealed

bash
# Check Vault status
kubectl exec -n discovery-stack vault-0 -- vault status

# Unseal Vault
kubectl exec -n discovery-stack vault-0 -- vault operator unseal <unseal-key>

Consul Cluster Not Forming

bash
# Check Consul logs
kubectl logs -n discovery-stack consul-0

# Check cluster members
kubectl exec -n discovery-stack consul-0 -- consul members

# Check Consul peers
kubectl exec -n discovery-stack consul-0 -- consul operator raft list-peers

Application Can't Access Vault

bash
# Test Kubernetes auth
kubectl exec -n applications prescription-service-xxx -- \
  curl -s http://vault.discovery-stack:8200/v1/auth/kubernetes/login \
  -d '{"jwt":"'$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)'","role":"ndp-app"}'

# Check Vault logs
kubectl logs -n discovery-stack vault-0

# Verify policy
kubectl exec -n discovery-stack vault-0 -- vault policy read ndp-app

Service Not Registering in Consul

bash
# Check Consul catalog
kubectl exec -n discovery-stack consul-0 -- consul catalog services

# Check service health
kubectl exec -n discovery-stack consul-0 -- consul catalog nodes -service=<service-name>

# Check Consul agent logs
kubectl logs -n discovery-stack -l app=consul-agent

Security Considerations

Vault

  • Seal Configuration: Use auto-unseal in production
  • Access Control: Implement strict policies
  • Audit Logging: Enable and monitor audit logs
  • Encryption: Enable transit secrets engine for app-level encryption
  • Backup: Regular snapshots of Vault data

Consul

  • ACL System: Enable and configure ACL tokens
  • TLS Encryption: Encrypt all Consul communication
  • Intentions: Define service-to-service communication rules
  • Gossip Encryption: Enable gossip protocol encryption
  • Network Segmentation: Use Consul Enterprise namespaces

High Availability

Consul HA

  • 3 or 5 server nodes for quorum
  • Anti-affinity rules to spread across availability zones
  • Health checks on all endpoints
  • Automated backups of Consul state

Vault HA

  • 3 or 5 Vault instances with Consul backend
  • Load balancer for active/standby routing
  • Auto-unseal for automatic recovery
  • Regular snapshots of Consul backend

Next Steps