# WorkloadEntry

> **Reviewed Version**: Istio 1.31.0
> **Last Updated**: September 11, 2026

WorkloadEntry is a resource for registering Virtual Machines (VMs) or bare-metal servers into the Istio service mesh. This enables workloads outside of Kubernetes to utilize the mesh's traffic management, security, and observability features.

## Table of Contents

1. [Overview](#overview)
2. [WorkloadEntry vs Kubernetes Pod](#workloadentry-vs-kubernetes-pod)
3. [Architecture](#architecture)
4. [Basic Usage](#basic-usage)
5. [ServiceEntry Integration](#serviceentry-integration)
6. [VM Registration Practical Guide](#vm-registration-practical-guide)
7. [Security Settings (mTLS)](#security-settings-mtls)
8. [Health Checks and Monitoring](#health-checks-and-monitoring)
9. [Advanced Configuration](#advanced-configuration)
10. [Troubleshooting](#troubleshooting)
11. [Best Practices](#best-practices)

## Overview

WorkloadEntry describes a workload; creating it does not install Envoy, bootstrap identity, create network connectivity, or publish a DNS record. This chapter uses sidecar-mode VM integration. Examples are independent; keep each ServiceEntry, selected WorkloadEntries, and ServiceAccount in the intended namespace. The diagrams show registry/configuration relationships, not extra network hops.

### What is WorkloadEntry?

WorkloadEntry is an Istio Custom Resource Definition (CRD) that registers workloads (VMs, bare-metal) outside the mesh into the Istio service mesh.

### Use Scenarios

![Architecture diagram showing legacy VMs and a bare-metal server registering with the istiod control plane, which delivers configuration to the Envoy sidecars in Kubernetes Pods while the VMs and Pod workloads talk over mTLS.](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/en/.gitbook/assets/en-service-mesh-istio-traffic-management-13-workload-entry-0.png)

[🔍 View interactive diagram](https://www.atomai.click/kubernetes-docs/archmaps/en-service-mesh-istio-traffic-management-13-workload-entry-0.html)

**Primary Use Cases**:
1. **Gradual Migration**: Incrementally migrate legacy applications to Kubernetes
2. **Hybrid Architecture**: Operate VMs and containers simultaneously
3. **Database Integration**: Include external databases in the mesh
4. **High-Performance Workloads**: Utilize specialized hardware like GPU servers

## WorkloadEntry vs Kubernetes Pod

### Comparison Table

| Characteristic | Kubernetes Pod | WorkloadEntry (VM) |
|----------------|---------------|-------------------|
| **Deployment Location** | Inside cluster | Outside cluster |
| **Envoy Injection** | Automatic (sidecar) | Manual installation |
| **Service Discovery** | Automatic (Service) | Manual (WorkloadEntry) |
| **IP Management** | Kubernetes CNI | Manual specification |
| **mTLS** | Automatic | Automatic (certificate deployment required) |
| **Health Checks** | Automatic (Liveness/Readiness) | Manual configuration |
| **Scaling** | HPA | Manual |
| **Operational Complexity** | Low | High |
| **Use Scenario** | Cloud-native apps | Legacy apps, specialized hardware |

### Traffic Flow Comparison

![Flowchart comparing how a client request reaches a Kubernetes Pod through automatic Service discovery against how it reaches a VM through a manually registered ServiceEntry and WorkloadEntry.](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/en/.gitbook/assets/en-service-mesh-istio-traffic-management-13-workload-entry-1.png)

[🔍 View interactive diagram](https://www.atomai.click/kubernetes-docs/archmaps/en-service-mesh-istio-traffic-management-13-workload-entry-1.html)

## Architecture

### VM Workload Architecture

![Architecture diagram showing a manually installed Envoy sidecar on a VM exchanging mTLS traffic with a Pod's auto-injected Envoy, while istiod pushes xDS config to both proxies and issues a certificate to the VM side.](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/en/.gitbook/assets/en-service-mesh-istio-traffic-management-13-workload-entry-2.png)

[🔍 View interactive diagram](https://www.atomai.click/kubernetes-docs/archmaps/en-service-mesh-istio-traffic-management-13-workload-entry-2.html)

### Key Components

1. **WorkloadEntry**: VM information registration (IP, ports, labels)
2. **ServiceEntry**: Service definition and WorkloadEntry reference
3. **Envoy Proxy**: Manually installed sidecar on VM
4. **istiod**: Configuration deployment and certificate management
5. **Service Account**: VM identity authentication

## Basic Usage

### WorkloadEntry Resource Definition

```yaml
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: legacy-api-vm-1
  namespace: production
spec:
  # VM's IP address
  address: 192.168.1.100

  # Labels for service selection
  labels:
    app: legacy-api
    version: v1.0
    tier: backend

  # Service account for mTLS authentication
  serviceAccount: legacy-api-sa

  # Ports to expose
  ports:
    http: 8080
    https: 8443
    metrics: 9090

  # Locality information (optional)
  locality: us-west-2/us-west-2a

  # Weight (for load balancing, optional)
  weight: 100

  # Network (multi-network environment, optional)
  network: vm-network
```

### Required Field Descriptions

| Field | Description | Example |
|-------|-------------|---------|
| **address** | Endpoint address (IP, or DNS with DNS resolution; may be omitted for a configured remote network) | `192.168.1.100` |
| **labels** | Labels for ServiceEntry matching | `app: legacy-api` |
| **serviceAccount** | SA for mTLS authentication | `legacy-api-sa` |
| **ports** | Port map to expose | `http: 8080` |

### Registering Multiple VMs

```yaml
# VM 1
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-1
  namespace: production
spec:
  address: 192.168.1.101
  labels:
    app: api-service
    version: v1
  serviceAccount: api-sa
  ports:
    http: 8080
---
# VM 2
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-2
  namespace: production
spec:
  address: 192.168.1.102
  labels:
    app: api-service
    version: v1
  serviceAccount: api-sa
  ports:
    http: 8080
---
# VM 3 (different version)
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-3
  namespace: production
spec:
  address: 192.168.1.103
  labels:
    app: api-service
    version: v2  # New version
  serviceAccount: api-sa
  ports:
    http: 8080
```

## ServiceEntry Integration

WorkloadEntry is always used together with ServiceEntry.

### Basic Integration Pattern

```yaml
# 1. Define service with ServiceEntry
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: legacy-api
  namespace: production
spec:
  hosts:
  - api.legacy.internal
  addresses:
  - 240.240.1.1  # Virtual IP
  ports:
  - number: 8080
    name: http
    protocol: HTTP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: legacy-api  # Match with WorkloadEntry labels
---
# 2. Register VM with WorkloadEntry
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: legacy-api-vm-1
  namespace: production
spec:
  address: 192.168.1.100
  labels:
    app: legacy-api  # Match with ServiceEntry
    version: v1
  serviceAccount: legacy-api-sa
  ports:
    http: 8080
```

### Operation Flow

![Sequence diagram showing a Kubernetes Pod resolving a virtual IP through Istio DNS, then the Envoy proxy matching a ServiceEntry and WorkloadEntry before opening an mTLS connection to the registered VM and returning its response.](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/en/.gitbook/assets/en-service-mesh-istio-traffic-management-13-workload-entry-3.png)

[🔍 View interactive diagram](https://www.atomai.click/kubernetes-docs/archmaps/en-service-mesh-istio-traffic-management-13-workload-entry-3.html)

### Load Balancing

Automatic load balancing when multiple WorkloadEntries exist:

```yaml
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: database-cluster
  namespace: vm-workloads
spec:
  hosts:
  - db.cluster.internal
  ports:
  - number: 5432
    name: postgresql
    protocol: TCP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: postgres
      tier: database
      role: primary
---
# Separate read-only replica service
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: database-replicas
  namespace: vm-workloads
spec:
  hosts:
  - db-replicas.cluster.internal
  ports:
  - number: 5432
    name: postgresql
    protocol: TCP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: postgres
      tier: database
      role: replica
---
# Primary DB
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-primary
  namespace: vm-workloads
spec:
  serviceAccount: vm-postgres-sa
  address: 10.0.1.100
  labels:
    app: postgres
    tier: database
    role: primary
  weight: 100  # Weight
---
# Replica DB 1
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-replica-1
  namespace: vm-workloads
spec:
  serviceAccount: vm-postgres-sa
  address: 10.0.1.101
  labels:
    app: postgres
    tier: database
    role: replica
  weight: 50
---
# Replica DB 2
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-replica-2
  namespace: vm-workloads
spec:
  serviceAccount: vm-postgres-sa
  address: 10.0.1.102
  labels:
    app: postgres
    tier: database
    role: replica
  weight: 50
```

Send writes only to the primary service and reads to replicas only when the application tolerates replication semantics. Generic endpoint weights do not understand PostgreSQL roles. Configure DNS capture/unique VIPs for same-port services, and bootstrap each VM with the stated ServiceAccount.

## VM Registration Practical Guide

### Prerequisites

1. **VM Requirements**:
   - Network: Can communicate with Kubernetes cluster
   - OS/CPU: a Linux distribution and architecture supported by the selected sidecar package; this example uses the Debian package
   - Connectivity: reach istiod xDS/CA through its exposed VM gateway (normally 15012), and route workload traffic over the chosen topology. 15017 is a Kubernetes webhook port, not a VM application requirement.

2. **Kubernetes Preparation**:
   - Istio installation complete
   - Create namespace for VM usage
   - Create ServiceAccount

Before generating bootstrap files, expose the existing control plane for VM access using the [official VM installation procedure](https://istio.io/latest/docs/setup/install/virtual-machine/). A cluster-only istiod Service is not enough. This walkthrough assumes routed Pod-to-VM connectivity in one logical network; use the documented east-west/network configuration for separated networks. The generated cluster ID must match istiod’s configuration.

### Step 1: Create ServiceAccount

```bash
# Create namespace
kubectl create namespace vm-workloads

# Create ServiceAccount
kubectl create serviceaccount vm-postgres-sa -n vm-workloads

# VM identity bootstrap does not require listing Kubernetes Secrets.
```

### Step 2: Prepare WorkloadGroup Bootstrap Input

WorkloadGroup serves as a template for multiple WorkloadEntries:

```yaml
apiVersion: networking.istio.io/v1
kind: WorkloadGroup
metadata:
  name: postgres-vms
  namespace: vm-workloads
spec:
  metadata:
    labels:
      app: postgres
      version: v14
  template:
    serviceAccount: vm-postgres-sa
    network: ""  # Same logical network in this walkthrough
    ports:
      postgresql: 5432
      metrics: 9187
```

### Step 3: Install Envoy on VM

#### Generate Automatic Installation Script

```bash
# Generate VM registration files with istioctl
umask 077
istioctl x workload entry configure \
  -f workloadgroup.yaml \
  -o vm-postgres-1 \
  --clusterID Kubernetes

# Generated files:
# - cluster.env: Cluster information
# - istio-token: Authentication token
# - mesh.yaml: Mesh configuration
# - root-cert.pem: Root certificate
# - hosts: /etc/hosts entries
```

#### Execute Installation on VM

```bash
# Run on the administration workstation before entering the VM shell
ssh user@192.168.1.100 'install -d -m 700 "$HOME/istio-bootstrap"'
scp vm-postgres-1/* user@192.168.1.100:istio-bootstrap/
ssh user@192.168.1.100

# From this point, run on the VM
VM_BOOTSTRAP_DIR="$HOME/istio-bootstrap"
curl -fsSLo istio-sidecar.deb \
  https://blob.istio.io/istio-release/releases/1.31.0/deb/istio-sidecar.deb
sudo dpkg -i istio-sidecar.deb
sudo install -d -o istio-proxy -m 0750 \
  /etc/certs /var/run/secrets/tokens /var/lib/istio/envoy /etc/istio/config /etc/istio/proxy
sudo install -o istio-proxy -m 0644 "$VM_BOOTSTRAP_DIR/root-cert.pem" /etc/certs/root-cert.pem
sudo install -o istio-proxy -m 0600 "$VM_BOOTSTRAP_DIR/istio-token" /var/run/secrets/tokens/istio-token
sudo install -o istio-proxy -m 0600 "$VM_BOOTSTRAP_DIR/cluster.env" /var/lib/istio/envoy/cluster.env
sudo install -o istio-proxy -m 0600 "$VM_BOOTSTRAP_DIR/mesh.yaml" /etc/istio/config/mesh
# Review/merge once; replace stale istiod entries rather than appending duplicates
cat "$VM_BOOTSTRAP_DIR/hosts" | sudo tee -a /etc/hosts >/dev/null
sudo systemctl enable --now istio
sudo systemctl status istio
```



### Step 4: Register WorkloadEntry

The commands above use manual registration. Apply this WorkloadEntry after the VM agent is bootstrapped; do not also create a duplicate manual entry for an auto-registered VM:

```yaml
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-vm-1
  namespace: vm-workloads
spec:
  address: 192.168.1.100
  labels:
    app: postgres
    version: v14
  serviceAccount: vm-postgres-sa
  ports:
    postgresql: 5432
    metrics: 9187
```

```bash
kubectl apply -f workloadentry.yaml
```

### Step 5: Create ServiceEntry

```yaml
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: postgres-service
  namespace: vm-workloads
spec:
  hosts:
  - postgres.vm.internal
  addresses:
  - 240.240.2.1
  ports:
  - number: 5432
    name: postgresql
    protocol: TCP
  - number: 9187
    name: metrics
    protocol: HTTP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: postgres
```

```bash
kubectl apply -f serviceentry.yaml
```

### Step 6: Test Connection

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: pg-client
  namespace: vm-workloads
  labels:
    sidecar.istio.io/inject: "true"
  annotations:
    proxy.istio.io/config: |
      proxyMetadata:
        ISTIO_META_DNS_CAPTURE: "true"
spec:
  containers:
  - name: postgres
    image: postgres:14
    command: ["sleep", "infinity"]
```

```bash
kubectl apply -f pg-client.yaml
kubectl wait -n vm-workloads --for=condition=Ready pod/pg-client --timeout=120s
kubectl exec -it pg-client -n vm-workloads -c postgres -- \
  psql -h postgres.vm.internal -U dbuser -d mydb
```

Save the Pod manifest as pg-client.yaml. The PostgreSQL client version is an application example, independent of the Istio version. Supply valid database credentials interactively. After applying the restrictive policy below, use an allowed ServiceAccount for tests; the default test identity will be denied. Remove the test Pod when finished.

## Security Settings (mTLS)

### Automatic mTLS Enablement

With the VM agent bootstrapped for the declared ServiceAccount, mesh proxies can use auto mTLS. WorkloadEntry alone does not turn a plain VM into a mesh participant:

```yaml
# Force mTLS with PeerAuthentication
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: vm-workloads
spec:
  mtls:
    mode: STRICT  # mTLS required for both VM and pod
```

### VM Identity Verification

Unlike pod-sidecar bootstrap, the VM integration persists its issued certificate/key under /etc/certs and uses the existing mTLS identity for renewal. These files appear after successful bootstrap. Keep keys private; regenerate bootstrap material only as part of a diagnosed recovery, not by printing or copying credentials into logs.

```bash
# Check certificates on VM
sudo ls -la /etc/certs/
# cert-chain.pem: Certificate chain
# key.pem: Private key
# root-cert.pem: Root CA

# Check certificate contents
sudo openssl x509 -in /etc/certs/cert-chain.pem -text -noout

# Check Subject Alternative Name (SAN):
# spiffe://cluster.local/ns/vm-workloads/sa/vm-postgres-sa
```

### Access Control (AuthorizationPolicy)

```yaml
# PostgreSQL access control
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: postgres-access
  namespace: vm-workloads
spec:
  selector:
    matchLabels:
      app: postgres  # WorkloadEntry labels
  action: ALLOW
  rules:
  # Allow only API service access
  - from:
    - source:
        principals:
        - cluster.local/ns/production/sa/api-service-sa
    to:
    - operation:
        ports: ["5432"]

  # Allow monitoring service access
  - from:
    - source:
        principals:
        - cluster.local/ns/istio-system/sa/prometheus
    to:
    - operation:
        ports: ["9187"]  # postgres_exporter
```

### mTLS Verification

```bash
# Use a PostgreSQL client in an authorized mesh workload; PostgreSQL is not HTTPS
kubectl exec -it <authorized-client-pod> -n production -c <app-container> -- \
  psql -h postgres.vm.internal -U dbuser -d mydb
# Inspect the client proxy's TLS transport and certificates separately
istioctl proxy-config clusters <authorized-client-pod> -n production --fqdn postgres.vm.internal -o json
istioctl proxy-config secret <authorized-client-pod> -n production
# On the VM, inspect public certificate information through its local admin interface
curl -fsS http://127.0.0.1:15000/certs
```

## Health Checks and Monitoring

### Health Check Configuration

For the optional automated workflow, the relevant control-plane flags are `PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION` and `PILOT_ENABLE_WORKLOAD_ENTRY_HEALTHCHECKS`; merge them into the existing installation settings as documented.

WorkloadGroup supports readiness probes. The documented auto-registration/health-check workflow requires enabling its istiod flags, applying the WorkloadGroup, and generating bootstrap files with --autoregister. It is an opt-in workflow, distinct from the manual path above. The DestinationRule below is passive connection-failure detection, not an active health probe:

```yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: postgres-healthcheck
  namespace: vm-workloads
spec:
  host: postgres.vm.internal
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
    outlierDetection:
      consecutive5xxErrors: 5  # Exclude after 5 consecutive failures
      interval: 30s         # Check every 30 seconds
      baseEjectionTime: 30s # Exclude for 30 seconds
      maxEjectionPercent: 50 # Exclude up to 50%
      minHealthPercent: 0    # Disable panic fail-open threshold
```

```yaml
# Optional WorkloadGroup probe fragment for the documented auto-registration workflow
spec:
  probe:
    initialDelaySeconds: 5
    periodSeconds: 5
    timeoutSeconds: 3
    tcpSocket:
      host: 127.0.0.1
      port: 5432
```

### VM Health Check Endpoint

Add a health check endpoint to your VM application:

```python
# Python Flask example
from flask import Flask, jsonify
import psycopg2

app = Flask(__name__)

@app.route('/health', methods=['GET'])
def health():
    try:
        # Verify database connection
        conn = psycopg2.connect("dbname=mydb user=dbuser", connect_timeout=3)
        conn.close()
        return jsonify({"status": "healthy"}), 200
    except psycopg2.Error:
        return jsonify({"status": "unhealthy"}), 503

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080)
```

### Prometheus Metrics Collection

ServiceMonitor discovers Kubernetes Services/endpoints, not WorkloadEntry or ServiceEntry directly. For this VM, deploy postgres_exporter on 9187, expose that named port in the WorkloadEntry/ServiceEntry, and add an explicit scrape job to the existing collector configuration. The collector must resolve the synthetic hostname, use mesh mTLS, and have an identity allowed by the exporter AuthorizationPolicy.

```yaml
# Fragment for the existing Prometheus scrape_configs
- job_name: workloadentry-postgres
  scrape_interval: 30s
  metrics_path: /metrics
  static_configs:
  - targets: ["postgres.vm.internal:9187"]
```

### Grafana Dashboard Queries

```promql
# Native PostgreSQL traffic has TCP counters, not HTTP status/latency metrics
sum(rate(istio_tcp_connections_opened_total{reporter="source",destination_service="postgres.vm.internal"}[5m]))
sum(rate(istio_tcp_sent_bytes_total{reporter="source",destination_service="postgres.vm.internal"}[5m]))

# Collector/exporter health; inspect the actual emitted labels
up{job="workloadentry-postgres"}
pg_up{job="workloadentry-postgres"}
```

## Advanced Configuration

### Multi-Network Environment

A network name only identifies topology. Configure reachability/east-west gateways and matching mesh network data separately; it does not create a VPC route, peering connection, or gateway. Locality is region/zone/subzone; use real values that agree with the client proxy.

Register VMs in different networks:

```yaml
# VM in Network A
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-network-a
spec:
  address: 192.168.1.100
  labels:
    app: api-service
  serviceAccount: api-sa
  network: network-a
  ports:
    http: 8080
---
# VM in Network B
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-network-b
spec:
  address: 10.0.1.100
  labels:
    app: api-service
  serviceAccount: api-sa
  network: network-b
  ports:
    http: 8080
```

### Locality-aware Load Balancing

```yaml
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-us-west
spec:
  address: 192.168.1.100
  labels:
    app: api-service
  locality: us-west-2/us-west-2a
  weight: 100
---
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-us-east
spec:
  address: 10.0.1.100
  labels:
    app: api-service
  locality: us-east-1/us-east-1a
  weight: 100
---
# Locality-aware routing with DestinationRule
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api-locality-lb
spec:
  host: api.service.internal
  trafficPolicy:
    loadBalancer:
      localityLbSetting:
        enabled: true
        distribute:
        - from: us-west-2/*
          to:
            "us-west-2/*": 80
            "us-east-1/*": 20
        - from: us-east-1/*
          to:
            "us-east-1/*": 80
            "us-west-2/*": 20
```

### Canary Deployment

Canary deployment can also be applied to WorkloadEntry:

```yaml
# v1 version VM
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-v1
spec:
  address: 192.168.1.100
  labels:
    app: api-service
    version: v1
  serviceAccount: api-sa
---
# v2 version VM (Canary)
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: api-vm-v2
spec:
  address: 192.168.1.101
  labels:
    app: api-service
    version: v2
  serviceAccount: api-sa
---
# Traffic splitting with VirtualService
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-service-canary
spec:
  hosts:
  - api.service.internal
  http:
  - route:
    - destination:
        host: api.service.internal
        subset: v1
      weight: 90
    - destination:
        host: api.service.internal
        subset: v2
      weight: 10
---
# Define subsets with DestinationRule
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api-service-subsets
spec:
  host: api.service.internal
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
```

## Troubleshooting

### WorkloadEntry Not Registered

**Symptom**: Resource visible in `kubectl get workloadentry` but traffic not routing

**Verification**:

```bash
# 1. Check WorkloadEntry status
kubectl get workloadentry -n vm-workloads -o yaml

# 2. Check ServiceEntry's workloadSelector
kubectl get serviceentry -n vm-workloads -o yaml | grep -A 5 workloadSelector

# 3. Verify label matching
# WorkloadEntry labels:
#   app: postgres
# ServiceEntry workloadSelector:
#   labels:
#     app: postgres  # Must match

# 4. Check Envoy configuration
istioctl proxy-config endpoints <pod-name> -n production

# Output should include WorkloadEntry's IP:
# ENDPOINT            STATUS      CLUSTER
# 192.168.1.100:5432  HEALTHY     outbound|5432||postgres.vm.internal
```

**Resolution**:

```yaml
# Modify labels to match exactly
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-vm-1
  namespace: vm-workloads
spec:
  address: 192.168.1.100
  serviceAccount: vm-postgres-sa
  labels:
    app: postgres  # Must be identical to ServiceEntry
    version: v14
```

### mTLS Connection Failure on VM

**Symptom**: `connection refused` or `TLS handshake failed`

**Verification**:

```bash
# Check Envoy logs on VM
sudo journalctl -u istio -f | grep -i tls

# Check certificates
sudo ls -la /etc/certs/
sudo openssl x509 -in /etc/certs/cert-chain.pem -text -noout

# Check certificate expiration
sudo openssl x509 -in /etc/certs/cert-chain.pem -noout -dates

# Check ServiceAccount token
sudo ls -la /var/run/secrets/tokens/
sudo stat /var/run/secrets/tokens/istio-token
```

**Resolution**:

```bash
# Regenerate bootstrap inputs from the admin workstation; this command does not issue a certificate
umask 077
istioctl x workload entry configure \
  -f workloadgroup.yaml \
  -o vm-postgres-1 \
  --clusterID Kubernetes

# Copy to VM and restart Envoy
scp vm-postgres-1/* user@192.168.1.100:istio-bootstrap/
# Reapply all reviewed runtime files and permissions using the VM installation steps,
# including the token and mesh configuration; then restart istio. Do not replace only the root CA.
```

### Traffic Not Reaching Due to Health Check Failure

**Symptom**: Envoy marks WorkloadEntry as `UNHEALTHY`

**Verification**:

```bash
# Check Envoy endpoint status
istioctl proxy-config endpoints <pod-name> -n production | grep postgres

# Output:
# 192.168.1.100:5432  UNHEALTHY  outbound|5432||postgres.vm.internal

# Check DestinationRule's outlierDetection
kubectl get destinationrule -n vm-workloads -o yaml
```

**Resolution**:

```yaml
# Adjust OutlierDetection settings
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: postgres-healthcheck
  namespace: vm-workloads
spec:
  host: postgres.vm.internal
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 10     # More lenient
      interval: 60s             # Increase check interval
      baseEjectionTime: 60s
      maxEjectionPercent: 50    # Bound ejection; 100 would allow excluding every endpoint
```

### DNS Lookup Failure

A ServiceEntry VIP alone does not create a CoreDNS record. Enable DNS capture on the calling sidecar and recreate its Pod, or provide a real DNS record. VM bootstrap enables the VM DNS proxy; that does not automatically enable capture on every Kubernetes client.

**Symptom**: `postgres.vm.internal` lookup fails from pod

**Verification**:

```bash
# Check ServiceEntry
kubectl get serviceentry -n vm-workloads

# DNS lookup test
kubectl exec pg-client -n vm-workloads -c postgres -- getent hosts postgres.vm.internal

# Check Istio DNS Proxy enablement
kubectl get pod <pod-name> -o yaml | grep ISTIO_META_DNS_CAPTURE
```

**Resolution**:

```yaml
# Add addresses to ServiceEntry
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: postgres-service
  namespace: vm-workloads
spec:
  hosts:
  - postgres.vm.internal
  addresses:
  - 240.240.2.1  # Specify virtual IP
  ports:
  - number: 5432
    name: postgresql
    protocol: TCP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: postgres
```

## Best Practices

### 1. Naming Conventions

```text
# WorkloadEntry name: <app>-<role>-<id>
name: postgres-primary-1
name: postgres-replica-2
name: api-backend-vm-3

# ServiceEntry name: <app>-service
name: postgres-service
name: api-service

# ServiceAccount name: <app>-sa
name: postgres-sa
name: api-sa
```

### 2. Label Strategy

```yaml
spec:
  labels:
    # Required labels
    app: postgres          # Application name
    version: v14           # Version

    # Optional labels
    tier: database         # Tier (frontend, backend, database)
    role: primary          # Role (primary, replica, canary)
    environment: production # Environment
    team: platform         # Team
```

### 3. ServiceAccount Management

```bash
# Separate ServiceAccount by namespace
kubectl create sa db-sa -n databases
kubectl create sa api-sa -n applications
kubectl create sa cache-sa -n middleware

# Principle of least privilege
kubectl create role db-limited \
  --verb=get \
  --resource=configmaps \
  -n databases
```

### 4. Monitoring and Alerting

```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: workloadentry-alerts
  namespace: vm-workloads
spec:
  groups:
  - name: workloadentry
    rules:
    - alert: VMExporterScrapeDown
      expr: up{job="workloadentry-postgres"} == 0 or absent(up{job="workloadentry-postgres"})
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "VM exporter scrape target is unavailable"
    - alert: PostgresExporterReportsDown
      expr: pg_up{job="workloadentry-postgres"} == 0
      for: 5m
      labels:
        severity: warning
```



### 5. Documentation

Maintain documentation for each WorkloadEntry:

```yaml
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: postgres-primary-1
  annotations:
    description: "Primary PostgreSQL database for production"
    owner: "platform-team@example.com"
    provisioned-date: "2025-11-26"
    os: "Ubuntu 22.04 LTS"
    location: "us-west-2a"
    runbook: "https://wiki.example.com/postgres-vm-runbook"
spec:
  address: 192.168.1.100
  labels:
    app: postgres
    version: v14
```

### 6. Backup and Disaster Recovery

Preserve the original WorkloadGroup/ServiceEntry/manual WorkloadEntry manifests and mesh version separately from diagnostic exports. Securely back up the VM’s persisted identity as required. Exported objects contain server metadata/status that must be reviewed before restore; auto-registered entries should be recreated by their controller, not duplicated manually. Restore namespaces/accounts and control-plane reachability first.

```bash
# Backup WorkloadEntry
kubectl get workloadentry -n vm-workloads -o yaml > workloadentries-backup.yaml

# Backup ServiceEntry
kubectl get serviceentry -n vm-workloads -o yaml > serviceentries-backup.yaml

# Restore
kubectl apply -f serviceentry.yaml
# Manual-registration path only: use the reviewed declarative source, not raw status snapshots
kubectl apply -f workloadentry.yaml
```

### 7. Gradual Migration Strategy

![Five-phase gradual migration flow that moves a VM workload registered with WorkloadEntry onto Kubernetes, with the phase-4 traffic transition highlighted as the key step.](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/en/.gitbook/assets/en-service-mesh-istio-traffic-management-13-workload-entry-4.png)

[🔍 View interactive diagram](https://www.atomai.click/kubernetes-docs/archmaps/en-service-mesh-istio-traffic-management-13-workload-entry-4.html)

Before these phases, bootstrap the legacy VM identity and create the shared ServiceEntry/subsets below. The Kubernetes deployment must be in vm-workloads with app=api and version=k8s, and its proxy/DNS configuration must be ready before traffic moves.

```yaml
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: api-migration-service
  namespace: vm-workloads
spec:
  hosts: [api.internal]
  addresses: [240.240.4.1]
  ports:
  - number: 8080
    name: http
    protocol: HTTP
  location: MESH_INTERNAL
  resolution: STATIC
  workloadSelector:
    labels:
      app: api
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api-migration-subsets
  namespace: vm-workloads
spec:
  host: api.internal
  subsets:
  - name: legacy
    labels:
      version: legacy
  - name: k8s
    labels:
      version: k8s
```

**Phase 1: VM Mesh Registration**
```yaml
# Register WorkloadEntry
apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: legacy-api-vm
  namespace: vm-workloads
spec:
  serviceAccount: api-sa
  address: 192.168.1.100
  labels:
    app: api
    version: legacy
```

**Phase 2: Traffic Splitting (100% VM)**
```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-migration
  namespace: vm-workloads
spec:
  hosts:
  - api.internal
  http:
  - route:
    - destination:
        host: api.internal
        subset: legacy
      weight: 100
```

**Phase 3: Kubernetes Deployment**
```bash
kubectl apply -n vm-workloads -f kubernetes-deployment.yaml
```

**Phase 4: Gradual Traffic Transition**
```yaml
# 10% Kubernetes
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-migration
  namespace: vm-workloads
spec:
  hosts:
  - api.internal
  http:
  - route:
    - destination:
        host: api.internal
        subset: legacy  # VM
      weight: 90
    - destination:
        host: api.internal
        subset: k8s     # Kubernetes
      weight: 10
```

**Phase 5: VM Removal**
```bash
# After transitioning 100% traffic to Kubernetes
kubectl delete workloadentry legacy-api-vm -n vm-workloads
```

## References

### Official Documentation
- [WorkloadEntry Reference](https://istio.io/latest/docs/reference/config/networking/workload-entry/)
- [WorkloadGroup Reference](https://istio.io/latest/docs/reference/config/networking/workload-group/)
- [Virtual Machine Installation](https://istio.io/latest/docs/setup/install/virtual-machine/)

### Related Documents
- [Basic Concepts - VM Workload Registration](https://www.atomai.click/kubernetes-docs/llms/en/service-mesh/istio/02-basic-concepts.md#vm-workload-registration)
- [ServiceEntry](https://www.atomai.click/kubernetes-docs/llms/en/service-mesh/istio/traffic-management/12-service-entry.md)
- [Egress Control](https://www.atomai.click/kubernetes-docs/llms/en/service-mesh/istio/traffic-management/11-egress-control.md)
- [Security - mTLS](https://www.atomai.click/kubernetes-docs/llms/en/service-mesh/istio/security/01-mtls.md)

### Additional Resources
- [Istio VM Integration Guide](https://istio.io/latest/blog/2020/workload-entry/)
- [Envoy Proxy Documentation](https://www.envoyproxy.io/docs/envoy/latest/)

- [Primary reference 1](https://istio.io/latest/docs/setup/install/virtual-machine/)
- [Primary reference 2](https://istio.io/latest/docs/ops/diagnostic-tools/virtual-machines/)
- [Primary reference 3](https://istio.io/latest/docs/reference/config/networking/workload-entry/)
- [Primary reference 4](https://istio.io/latest/docs/reference/config/networking/workload-group/)
- [Primary reference 5](https://istio.io/latest/docs/reference/config/security/authorization-policy/)
- [Primary reference 6](https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/)
- [Primary reference 7](https://prometheus-operator.dev/docs/api-reference/api/)
- [Primary reference 8](https://istio.io/latest/docs/reference/config/networking/destination-rule/)
