Skip to content

Exposing Workloads

Getting traffic from outside to a workload in your cluster.

Three patterns, and the first one is the only one with anything kMetal-specific about it:

Pattern Reaches
LoadBalancer Service The outside world, any TCP/UDP port Needs an address from your Environment — below
Ingress The outside world, HTTP/HTTPS with host and path routing Ordinary Kubernetes, on a controller you install — below
NodePort Inside your own VPC only Not a public surface — below

LoadBalancer Services

An external address is granted, not self-served.

The addresses your tenant holds are EipClaim objects in your Environment, and a Service binds to one of them. A cluster with no free claim cannot publish a LoadBalancer Service at all — it will wait, with an event saying so.

So the flow is: see what you have, then bind to it.

See what you have

Run this with your Environment credentials, not your cluster's kubeconfig:

$ kubectl get eipclaims -n dynamo-prod
NAME       EIP                    V4              BOUND   READY   AGE
external   dynamo-prod-external   198.51.100.22   true    True    47h
web        dynamo-prod-web        198.51.100.24   false   True    47h

Read three columns:

  • READY — the address is allocated in the platform's network. A claim that is not Ready cannot be bound to anything.
  • BOUND — something already holds it. Bindings are exclusive, one Service per claim.
  • V4 — the address itself, which is what goes in DNS.

external is not yours to bind a Service to

The claim named external is your VPC's own egress address — the source address your cluster's outbound traffic appears to come from. It is created and held by your VPC, and a workload cannot take it.

Every other claim is a nat claim, and those are the ones a Service binds.

Bind a Service to one

Two things are required. Both are easy to leave out, and each has a distinct symptom.

apiVersion: v1
kind: Service
metadata:
  name: my-app
  annotations:
    network.kmetal.io/eip-claim: web       # which claim to bind
spec:
  type: LoadBalancer
  loadBalancerClass: kmetal                # who fulfils it
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

spec.loadBalancerClass: kmetal is what hands the Service to the platform. Without it, kMetal ignores the Service entirely and EXTERNAL-IP stays <pending> forever with no event — because as far as the platform is concerned you asked some other implementation to serve it. This is deliberate: it leaves any LoadBalancer implementation you install in your own cluster completely alone.

network.kmetal.io/eip-claim picks the claim by name. Leave it out and the Service binds to the first free Ready nat claim in the Environment, in name order — fine when you hold one address, ambiguous when you hold several.

Name the claim whenever the address matters. It is what keeps a DNS record pointing at the same workload across a Service being deleted and recreated.

$ kubectl get svc my-app
NAME     TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
my-app   LoadBalancer   10.96.31.42    198.51.100.24    80:31234/TCP   12s

Where the traffic actually goes

Traffic for that address arrives on the platform's provider network, is translated by OVN, and is delivered straight to your pod.

The under cluster is not a hop in the data path. There is no proxy, no per-Service controller inside your cluster, and no platform component sitting in the middle of your traffic that can fall over independently of it.

A Service that never gets an address

Symptom Cause
<pending>, no events at all spec.loadBalancerClass: kmetal is missing. The platform is not looking at this Service.
no free EipClaim available in namespace "..." on the Service Every claim in your Environment is already bound. Free one, or ask for another.
Waiting, with a warning event on the claim The claim you named is held by another Service. Bindings are exclusive.
Denied at kubectl apply Platform-side: the admission webhook that resolves the binding is unreachable. Report it.
kubectl describe svc my-app                        # in your cluster
kubectl get eipclaims -n dynamo-prod               # in your Environment

Getting another address

If you hold the networking role on your Environment, an EipClaim is yours to create:

apiVersion: network.kmetal.io/v1alpha1
kind: EipClaim
metadata:
  name: api
  namespace: dynamo-prod
spec:
  type: nat

Leave spec.v4Ip out and the platform allocates an address for you. Set it only to pin an address your DNS or edge router already knows about — and only to one inside the range your platform team gave you, or the claim never becomes Ready.

kubectl apply -f eipclaim.yaml
kubectl get eipclaim api -n dynamo-prod -o jsonpath='{.status.conditions}'

Two constraints:

  • spec is immutable. Re-addressing means deleting the claim and creating a new one, which takes the address's NAT rules with it — so the Service goes down for the duration.
  • How many you may hold is quota'd. A claim over quota is refused when you create it. Routable addresses are the scarcest thing on most platforms, so treat an exhausted quota as a conversation rather than a bug.

If kubectl apply is refused outright, you do not hold the role — ask your platform team to create the claim, or to grant it.

Releasing one

Delete the Service first, or drop the annotation. Deleting a bound claim is rejected, which is what stops an address disappearing from under a running workload.

Ingress

For many HTTP services behind one address, run an ingress controller in your own cluster. The platform does not provide one, and does not want to — which controller, which version and how it is configured is yours.

The shape is ordinary Kubernetes:

  1. Install the controller of your choice.
  2. Give its front-end Service the two fields from above — loadBalancerClass: kmetal and, ideally, a named claim. That Service is now your single public address.
  3. Create Ingress objects. They never touch kMetal.

That is the shape to prefer for HTTP. One EipClaim fronts every hostname you serve, so it costs one address rather than one per service — and addresses are the resource most likely to be capped.

NodePort and headless Services

NodePort publishes on your worker VMs' addresses inside your own VPC. Nothing outside your VPC can route to it, so it is not a public surface — cross-VPC traffic is dropped, and so is traffic toward the platform's own addresses.

Use it for something reachable from elsewhere inside your own network, not for user traffic.

Headless Services (clusterIP: None) behave exactly as they do anywhere, and are the right choice for a StatefulSet whose clients resolve pod addresses directly.

What your cluster cannot reach

Two boundaries are enforced below your cluster, so they are worth knowing before you spend time debugging a connection that is never going to work:

  • Other tenants. No route exists between your VPC and anyone else's. Knowing an address does not help.
  • The platform itself. Traffic from your workloads toward the under cluster's API server and platform services is blocked.

Your own egress to the internet, and to whatever your platform team has routed, works normally.


See Also: Persistent Storage · Deploying Applications · Networking for the VPC model