Skip to main content

Service Exposure Guide

This guide explains how to expose workloads in a Kube-DC Project. Choose the method by protocol and reachability: use a Gateway route for hostname-based web traffic, a LoadBalancer Service for selected TCP or UDP ports, and a Floating IP for direct access to a VM. Both Project network types support these methods.

Managed Clusters: every annotation below also works on LoadBalancer services inside a Managed Cluster — the cloud controller manager copies them to the platform cluster at Service creation time. See Cluster Management for the Managed Cluster specifics (Issuer prerequisite, hostname pinning, public-IP quota).

Quick Reference

NeedRecommended methodResult
HTTP or HTTPS hostnameGateway route (expose-route)Shared gateway, DNS name, optional automatic TLS
Selected TCP or UDP portsLoadBalancer Service + EIPAddress and ports dedicated to the Service
Direct VM accessFloating IPOne-to-one NAT to the VM

Note: Both network types support EIPs and LoadBalancers. The difference is where EIPs are allocated from.

Understanding Project Network Types

Every installation supports the cloud type. The public Project type is available only when the provider enables it; otherwise the dashboard hides it and the API rejects it. When both are offered, choose an egressNetworkType:

apiVersion: kube-dc.com/v1
kind: Project
metadata:
name: production
namespace: acme
spec:
egressNetworkType: cloud # or "public"

This creates Project production in Organization acme. Kubernetes stores that Project's workload resources in the backing namespace acme-production.

Cloud Network (egressNetworkType: cloud)

  • Default EIPs allocated from the cloud address pool
  • Outbound traffic is SNATed through the Project gateway EIP
  • Can create public EIPs by specifying externalNetworkType: public when the provider exposes that pool and quota is available
  • Gateway Routes provide easy HTTPS exposure with auto-certificates
  • Supports VMs, pods, and all workload types
  • Best for: Web applications, APIs, microservices, and internal platform connectivity
  • Cost: Provider- and plan-dependent

Public Network (egressNetworkType: public)

  • Default EIPs allocated from the configured public address pool
  • Outbound traffic is SNATed through the Project gateway EIP
  • The default gateway EIP is allocated from the public address pool
  • Supports any TCP/UDP protocol
  • Supports VMs, pods, and all workload types
  • Best for: Game servers, custom protocols, direct IP requirements
  • Cost: Provider- and plan-dependent; public address quota still applies

Feature Comparison

FeatureCloud ProjectPublic Project
Default EIP sourceConfigured cloud address poolConfigured public address pool
Can get public EIPsWhen the provider exposes the pool and quota is availableYes by default, subject to quota
Can use Gateway Routes✅ Yes✅ Yes
Can use EIP + LB✅ Yes✅ Yes
Can run VMs✅ Yes✅ Yes
Can run Pods✅ Yes✅ Yes

Part 1: Gateway Routes

Use Gateway routes for hostname-based HTTP, HTTPS, or TLS passthrough in either Project network type.

All Service Annotations Reference

Gateway Route Annotations

AnnotationDescriptionExample Values
expose-routeEnable Gateway routehttp, https, tls-passthrough
route-hostnameCustom hostname (optional)api.example.com
route-portTarget port (optional)8080, 50051
tls-issuercert-manager Issuer nameletsencrypt (default)
tls-secretUser-provided TLS secretmy-tls-secret

EIP/LoadBalancer Annotations

AnnotationDescriptionExample Values
bind-on-default-gw-eipUse project's default EIP"true"
bind-on-eipUse a specific EIP by namemy-eip
autodeleteDelete the Service if it remains without endpoints; advanced recovery behavior, not EIP lifecycle"true"
create-gateway-backendCreate Envoy Gateway backend"true"

Note: Prefix is service.nlb.kube-dc.com/

warning

autodelete does not manage EIP cleanup. It can delete the Service when the Service remains without endpoints. Leave it unset for normal workload lifecycle.

Network Type Annotation

AnnotationDescriptionExample Values
network.kube-dc.com/external-network-typeEIP type for auto-created EIP (set at creation, immutable)cloud, public

Tip: Use this on a LoadBalancer service to get a public EIP in a cloud project:

annotations:
network.kube-dc.com/external-network-type: "public"

Set this when you create the Service — it cannot be changed afterwards. The annotation chooses which external network the Service's address is allocated from, and an external IP keeps the type it was allocated with for life. Editing the annotation on a Service that already has an address is rejected, so you get a clear error instead of a change that appears to work and does nothing.

To move a workload to a different external network, create a second Service with the annotation you want, cut traffic over to its address, then delete the old one. In that order the workload is never without a reachable address — deleting first would release the old IP before the new one is serving.

Status Annotations (Read-Only)

AnnotationDescription
route-hostname-statusAssigned hostname (set by controller)

Note: All annotations use prefix service.nlb.kube-dc.com/

Gateway Route Annotations (Details)

Add these annotations to your LoadBalancer Service.

Multi-Port Services

When using expose-route, the gateway routes traffic to a single port on your Service. By default this is the first port in spec.ports. For every multi-port Service, set route-port explicitly so a manifest reorder cannot silently change the routed backend.

If your application listens on a non-standard port, use the route-port annotation to specify which port the gateway should target:

annotations:
service.nlb.kube-dc.com/expose-route: "https"
service.nlb.kube-dc.com/route-port: "8080"

Note: This applies to all route types (http, https, tls-passthrough). The gateway terminates TLS (for https) or passes it through (for tls-passthrough), then forwards traffic to the selected port on your service.

Route Type Comparison

Route TypePortTLSApp ServesUse Case
http80NoneHTTPPlain HTTP traffic
https443Gateway terminatesHTTPRecommended for web traffic; automatic TLS
tls-passthrough443App terminatesHTTPSEnd-to-end encryption

The simplest way to expose a web app with automatic TLS:

Step 1: Create the Issuer (once per Project)

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: acme-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # Replace with valid email
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: eg
namespace: envoy-gateway-system

Step 2: Deploy your application

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: acme-production
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: nginx:alpine
ports:
- containerPort: 80

Step 3: Create LoadBalancer Service with HTTPS route

apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: acme-production
annotations:
# Expose via HTTPS with auto-provisioned certificate
service.nlb.kube-dc.com/expose-route: "https"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 80

Step 4: Verify and access

# Check assigned hostname
kubectl get svc my-app -n acme-production -o jsonpath='{.metadata.annotations.service\.nlb\.kube-dc\.com/route-hostname-status}'
# Output: my-app-acme-production.kube-dc.cloud

# Check certificate status
kubectl get certificate -n acme-production
kubectl get challenge -n acme-production

# Test access
curl https://my-app-acme-production.kube-dc.cloud

For HTTPS routes, hostname status is set after the certificate and route are ready. This can take a few minutes. If the command returns an empty value, check the certificate and ACME challenge status first.

Example: Plain HTTP

For non-TLS HTTP traffic:

apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: acme-production
annotations:
service.nlb.kube-dc.com/expose-route: "http"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 80

Access via: http://my-app-acme-production.kube-dc.cloud

Example: TLS Passthrough (Kubernetes API)

For services that handle their own TLS (like Kubernetes control planes):

apiVersion: v1
kind: Service
metadata:
name: cluster-api
namespace: acme-production
annotations:
service.nlb.kube-dc.com/expose-route: "tls-passthrough"
spec:
type: LoadBalancer
selector:
app: kube-apiserver
ports:
- port: 6443
targetPort: 6443

The Gateway listens publicly on port 443 and forwards to Service port 6443: https://cluster-api-acme-production.kube-dc.cloud.

Configure backend TLS first

TLS passthrough does not issue a certificate or terminate TLS. The TLSRoute selects a backend from the SNI hostname in the client's initial TLS handshake. The client must therefore connect by the published hostname, and the backend must present a certificate whose SANs include that hostname. For this example, add cluster-api-acme-production.kube-dc.cloud to the API server certificate before exposing it. Use a dedicated LoadBalancer address when the protocol does not start with a TLS handshake or the backend certificate cannot cover the Gateway hostname.

Example: Custom Hostname

Override the auto-generated hostname:

apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: acme-production
annotations:
service.nlb.kube-dc.com/expose-route: "https"
service.nlb.kube-dc.com/route-hostname: "api.mycompany.com"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 80

Note: You must configure DNS to point api.mycompany.com to the Gateway IP.

Example: User-Provided Certificate

Use your own TLS certificate instead of auto-provisioning:

# First, create your TLS secret
kubectl create secret tls my-tls-secret \
--cert=path/to/tls.crt \
--key=path/to/tls.key \
-n acme-production
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: acme-production
annotations:
service.nlb.kube-dc.com/expose-route: "https"
service.nlb.kube-dc.com/tls-secret: "my-tls-secret"
service.nlb.kube-dc.com/route-hostname: "secure.mycompany.com"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 80

The certificate in my-tls-secret must include secure.mycompany.com in its SANs and chain to a CA trusted by your clients.

gRPC

The expose-route annotation currently creates an HTTPRoute, even when a Service port sets appProtocol: kubernetes.io/h2c. It does not create a GRPCRoute or configure an HTTP/2 backend. For gRPC today, use a dedicated LoadBalancer Service or work with your platform operator to provide explicit Gateway API GRPCRoute and backend protocol resources.


Part 2: EIP-Based Exposure (Both Project Types)

Both cloud and public projects can use EIPs and LoadBalancer services.

Default EIP Allocation

Project TypeDefault EIP SourceCan Request
CloudConfigured cloud address poolcloud; public when the provider exposes that pool and quota is available
PublicConfigured public address poolpublic; other types depend on provider configuration and quota

When to use EIPs vs Gateway Routes:

  • Use Gateway Routes for HTTP/HTTPS/TLS passthrough (automatic TLS for HTTPS)
  • Use EIPs for gRPC and other TCP/UDP protocols, VMs, or when you need a dedicated IP

Understanding EIPs

External IPs (EIPs) provide addresses for your Project from a provider-configured external network.

Default Gateway EIP

Every Project automatically gets a default EIP (default-gw) for outbound SNAT. A LoadBalancer Service uses it only when you set bind-on-default-gw-eip: "true"; otherwise the platform can allocate a Service-specific EIP.

Creating Additional EIPs

For services that need dedicated IPs:

apiVersion: kube-dc.com/v1
kind: EIp
metadata:
name: web-server-eip
namespace: acme-production
spec:
externalNetworkType: public # or "cloud"

EIP Types:

externalNetworkTypeDescriptionUse Case
cloudAddress from the cloud networkInternal platform connectivity and outbound SNAT
publicDedicated public IPDirect access, static IP, VMs

Tip: When your provider offers public addresses to cloud Projects, request a public EIP for workloads that need a dedicated internet-routable IP.

LoadBalancer Service Annotations

AnnotationDescription
service.nlb.kube-dc.com/bind-on-default-gw-eip: "true"Use project's default EIP
service.nlb.kube-dc.com/bind-on-eip: "eip-name"Use a specific EIP

Example: Web Server on Default EIP

apiVersion: v1
kind: Service
metadata:
name: nginx-lb
namespace: acme-production
annotations:
service.nlb.kube-dc.com/bind-on-default-gw-eip: "true"
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443

Example: Service on Dedicated EIP

# Step 1: Create dedicated EIP
apiVersion: kube-dc.com/v1
kind: EIp
metadata:
name: api-eip
namespace: acme-production
spec:
externalNetworkType: public
---
# Step 2: Bind service to the EIP
apiVersion: v1
kind: Service
metadata:
name: api-lb
namespace: acme-production
annotations:
service.nlb.kube-dc.com/bind-on-eip: "api-eip"
spec:
type: LoadBalancer
selector:
app: api-server
ports:
- port: 443
targetPort: 443

Example: VM SSH Access

Expose SSH access to a virtual machine:

apiVersion: v1
kind: Service
metadata:
name: vm-ssh
namespace: acme-production
annotations:
service.nlb.kube-dc.com/bind-on-default-gw-eip: "true"
spec:
type: LoadBalancer
selector:
vm.kubevirt.io/name: my-vm # Target VM name
ports:
- name: ssh
port: 2222 # External port
targetPort: 22 # Internal SSH port

Floating IPs (FIPs)

Floating IPs map an internal IP directly to an EIP, providing 1:1 NAT. For detailed FIP management, see External & Floating IPs.

When to Use FIPs

  • Direct IP mapping for VMs
  • Whole-VM exposure across many ports
  • Protocols that are awkward to model as individual Service ports

Creating a FIP for a VM

Use vmTarget to point a FIP at a VM. The controller reads the named interface address from the running VirtualMachineInstance status, so the VM must be running and that interface must report an IP:

apiVersion: kube-dc.com/v1
kind: FIp
metadata:
name: vm-fip
namespace: acme-production
spec:
externalNetworkType: public
vmTarget:
vmName: ubuntu
interfaceName: vpc_net_0

Important Limitation: FIP and LoadBalancer Conflicts

A pod/VM cannot simultaneously serve as:

  1. A target for a public FIP
  2. A backend for a cloud-network LoadBalancer service

This is because public FIPs create source-based policy routes that redirect ALL outbound traffic from that IP to the public gateway, breaking cloud-network services.

Example conflict:

Public FIP and cloud LoadBalancer conflictOne Pod or virtual machine at internal address 10.0.0.30 is simultaneously selected by a public Floating IP and a cloud-network LoadBalancer Service. The public FIP creates source-based routing toward public gateway 198.51.100.1, while the cloud LoadBalancer expects return traffic through cloud gateway 100.65.0.1. The conflicting asymmetric paths break the service.Pod or VM10.0.0.30Public pathpublic FIPgateway 198.51.100.1Cloud pathcloud LoadBalancergateway 100.65.0.1BROKEN: conflicting source routingChoose one exposure method or keep both paths on the same network type.
This is an unsupported mixed-gateway combination: a public FIP forces the workload's return path toward the public gateway while the cloud LoadBalancer expects the cloud gateway.

Workarounds:

  • Use separate pods for FIP targets and cloud-service backends
  • Use the same network type for both (all public or all cloud)
  • Choose one exposure method per pod

Part 3: Choosing the Right Approach

Decision Tree

Choose a Kube-DC exposure methodThe exposure decision starts with the workload and protocol. A web application or API uses an HTTPS Gateway Route with automatic hostname and TLS after Issuer setup. Direct SSH or RDP access to a virtual machine uses a dedicated EIP and Floating IP mapping. A custom TCP, UDP, or gRPC service uses a dedicated EIP and LoadBalancer Service with explicit ports. Multiple Services that intentionally share one external address can use the default gateway EIP with distinct ports where the platform supports that pattern.Exposure needworkload + protocolGateway Routeweb app / APIHTTPS + TLSFloating IPVM direct accessEIP + FIPLoadBalancerTCP / UDPEIP + LBShared gatewaymultiple Servicesshared EIP · portsKeep network type and return path consistentDo not combine public-FIP and cloud-LoadBalancer return paths on one endpoint.
Choose by traffic shape: hostname-based web traffic uses a Gateway Route, direct VM access uses a Floating IP, and arbitrary declared TCP or UDP ports use an EIP-backed LoadBalancer.

Comparison Table

FeatureGateway route (any Project)EIP + LoadBalancer (any Project)
IP AddressShared Gateway IPDedicated per EIP
ProtocolsHTTP, HTTPS, TLS passthroughAny TCP/UDP
TLS TerminationGateway (auto-cert)Application
CostProvider- and plan-dependentProvider- and plan-dependent
SetupSimple annotationEIP + Service config
DNSAuto hostnameManual
Best ForWeb apps, APIsVMs, game servers

Part 4: Advanced Topics

Envoy Gateway Backend

Use the create-gateway-backend annotation on a LoadBalancer Service to register an Envoy Gateway Backend for advanced routing scenarios. Prefer expose-route unless you are also managing the route yourself.

apiVersion: v1
kind: Service
metadata:
name: my-backend
namespace: acme-production
annotations:
service.nlb.kube-dc.com/create-gateway-backend: "true"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 8080
targetPort: 8080

This creates an Envoy Gateway Backend resource, enabling:

  • Cross-namespace routing from Gateway
  • Custom backend policies
  • Advanced load balancing configurations

Namespace-Scoped Ingress Controller

For advanced HTTP routing beyond Gateway capabilities, deploy a dedicated ingress-nginx:

This chart creates namespace-scoped Roles and RoleBindings, so installation requires the Project admin role. A developer can operate supported workload resources but cannot install this RBAC. Render and validate the chart against the server before installing it.

# ingress-values.yaml
controller:
ingressClassResource:
enabled: false
scope:
enabled: true
namespace: acme-production
admissionWebhooks:
enabled: false
service:
annotations:
service.nlb.kube-dc.com/bind-on-default-gw-eip: "true"
rbac:
create: true
scope: true
defaultBackend:
enabled: false
helm install ingress ingress-nginx/ingress-nginx \
--namespace acme-production \
--values ingress-values.yaml

Troubleshooting

Gateway Routes

# Check route hostname was assigned
kubectl get svc my-app -o yaml | grep route-hostname-status

# Check certificate status
kubectl get certificate -n acme-production
kubectl describe certificate my-app-tls -n acme-production

# Check HTTPRoute created
kubectl get httproute -n acme-production

The platform Gateway and controller run outside your Project permissions. If the Project resources above are healthy but the route still fails, provide the Service name and Project name to support.

EIP and LoadBalancer

# Check EIP status
kubectl get eip -n acme-production
kubectl describe eip my-eip -n acme-production

# Check LoadBalancer external IP
kubectl get svc -n acme-production

# Check service events
kubectl describe svc my-lb -n acme-production

Common Issues

IssueCauseSolution
No hostname assignedMissing expose-route annotationAdd annotation
Hostname status empty for HTTPSCertificate is still pendingCheck kubectl get certificate,challenge -n acme-production
Certificate not readyIssuer not created, ACME challenge pending, or quota prevents solver pod creationCreate Issuer first and make sure the project has free CPU/memory for cert-manager HTTP-01 solver pods
503 errorBackend not readyCheck pod status
EIP pendingNo available IPsCheck subnet capacity
Connection timeoutDNS not configuredPoint DNS to Gateway/EIP
Cloud LB stopped working after FIP createdFIP policy route conflictUse separate pods or delete FIP (see limitation)

Summary

NeedResource or annotationResult
Automatic HTTPSLoadBalancer + expose-route: httpsGateway terminates TLS and assigns a hostname
Plain HTTPLoadBalancer + expose-route: httpGateway serves HTTP
End-to-end TLSLoadBalancer + expose-route: tls-passthroughApplication terminates TLS; public listener is 443
Reuse Project gateway EIPLoadBalancer + bind-on-default-gw-eip: "true"Selected ports share the gateway address
Use a selected EIPLoadBalancer + bind-on-eip: "name"Selected ports use that address
Direct VM mappingFIpOne-to-one NAT to the VM