A `type: LoadBalancer` Service places one application on the internet on any TCP-based protocol. The Hetzner cloud-controller-manager (CCM) watches for these Services, creates a Hetzner load balancer for each one, and writes its public address back to the Service. > [!NOTE] > Hetzner load balancers carry TCP only (they also serve HTTP and HTTPS, which run on TCP). UDP is not supported: a Service with a UDP port receives no working load balancer. To expose a UDP service, use a `NodePort` on the nodes directly and manage the address and failover yourself. See [UDP is not supported](/docs/hetzner/apalla/network/expose/choose-how-to-expose#udp-is-not-supported). ## Prerequisites - kubectl access to the workload cluster. - At least one workload pod running behind the Service. ## Create the Service Set `type: LoadBalancer` and point the selector at your pods. The two annotations specify where the CCM places the load balancer. ```yaml title="service.yaml" apiVersion: v1 kind: Service metadata: name: my-app annotations: load-balancer.hetzner.cloud/location: fsn1 load-balancer.hetzner.cloud/name: my-app-lb spec: type: LoadBalancer selector: app: my-app ports: - name: http port: 80 targetPort: 8080 ``` Apply it and read the address the CCM assigns: ```console $ kubectl apply -f service.yaml $ kubectl get svc my-app NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-app LoadBalancer 100.96.42.17 80:31820/TCP 5s ``` `EXTERNAL-IP` shows `` until the CCM finishes creating the load balancer, usually within a minute. Read the assigned IPv4 address once it appears: ```console $ kubectl get svc my-app -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` The load balancer also receives a public IPv6 address. Both appear in `status.loadBalancer.ingress`. To serve IPv6 clients, publish an `AAAA` record for the IPv6 address. See [Serve IPv6 clients](/docs/hetzner/apalla/network/load-balancing/serve-ipv6). ## The Service only routes to ready pods A `type: LoadBalancer` Service forwards to a pod only while that pod reports **Ready**, so configure a readiness probe for the pod. Without one, Kubernetes considers the pod ready as soon as its container starts, and the load balancer sends live traffic before the application can serve it. This matters more here than on a static fleet, because the platform drains and replaces nodes during upgrades and self-healing, so pods are created and removed more frequently. [Run a production-ready workload](/docs/hetzner/apalla/workloads/production/run-a-production-ready-workload) covers the probe together with the replica count and PodDisruptionBudget the application needs. > [!WARNING] > The CCM reaches your pods through a NodePort, a port opened on every node. Cilium forwards NodePort traffic in eBPF before the host firewall processes it, so the assigned NodePort responds on every node's public IP from anywhere on the internet. Treat the NodePort as public: rely on the Service's own authentication or a [network policy](/docs/hetzner/apalla/security/segment-with-network-policies), not on the firewall, to protect it. ## Next - [Configure a Hetzner load balancer](/docs/hetzner/apalla/network/load-balancing/configure-a-load-balancer) for the full annotation reference: type, algorithm, and health checks. - [Preserve the client source IP](/docs/hetzner/apalla/network/load-balancing/preserve-client-source-ip) so the pod sees the caller's address instead of the load balancer's. - [Choose how to expose an application](/docs/hetzner/apalla/network/expose/choose-how-to-expose) to weigh this against an ingress controller once you expose more than one application.