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 notReadycannot 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:
specis 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:
- Install the controller of your choice.
- Give its front-end Service the two fields from above —
loadBalancerClass: kmetaland, ideally, a named claim. That Service is now your single public address. - Create
Ingressobjects. 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