Skip to content
docktionary

Kubernetes

Services & Ingress

Pods are mortal and get a new IP every time they're recreated. A Service gives a label-selected group of them one stable address; Ingress routes HTTP into those Services from outside the cluster.

ClusterIP — the default

A stable virtual IP, reachable only from inside the cluster, load-balancing across every Pod matching the selector.

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: docktionary-app
spec:
  type: ClusterIP
  selector:
    app: docktionary
  ports:
    - port: 80
      targetPort: 3000

Any Pod in the cluster can now reach this one with http://docktionary-app (or the fully qualified docktionary-app.default.svc.cluster.local), regardless of which Pods are currently backing it.

NodePort

Opens a fixed port (30000–32767 by default) on every node's own IP, forwarding into the Service. Rarely used directly in production — mostly a building block for LoadBalancer, or a quick way to poke a cluster from outside during development.

yaml
spec:
  type: NodePort
  selector:
    app: docktionary
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080

LoadBalancer

On a cloud-managed cluster, this provisions an actual external load balancer (an AWS NLB/ALB, a Google Cloud Load Balancer, an Azure Load Balancer) and points it at the Service.

yaml
spec:
  type: LoadBalancer
  selector:
    app: docktionary
  ports:
    - port: 80
      targetPort: 3000

Give every public-facing Service its own LoadBalancer and the cloud bill (and the number of IPs you're managing) grows with every service. Ingress exists to put several services behind one.

Ingress

An Ingress resource declares host- and path-based HTTP routing rules. An Ingress controller — nginx-ingress, Traefik, or a cloud-managed one — actually implements them, usually sitting behind a single LoadBalancer Service.

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: docktionary
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: ["docktionary.com"]
      secretName: docktionary-tls
  rules:
    - host: docktionary.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: docktionary-app
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: docktionary-api
                port:
                  number: 80

Gateway API: the newer alternative to Ingress

Ingress has a real limitation: it's just host/path rules, so anything more specific — traffic splitting, header-based routing, TCP/UDP instead of just HTTP — ends up as an implementation-specific annotation that only works with one particular controller. Gateway API is the successor, modeling routing as proper typed resources instead of annotations, and splitting the job into a GatewayClass (the controller), a Gateway (a listener — an IP and port), and one or more HTTPRoutes attached to it.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: docktionary
spec:
  parentRefs:
    - name: shared-gateway
  rules:
    - matches:
        - path:
            value: /api
      backendRefs:
        - name: docktionary-api
          port: 80
    - backendRefs:
        - name: docktionary-app
          port: 80

Ingress isn't deprecated and still covers most simple cases fine — Gateway API is worth reaching for once you need routing logic Ingress’s annotations can't express portably, or you want one config format that works the same way across different controllers.

Headless Services

Setting clusterIP: None turns off the load-balancing virtual IP entirely. DNS lookups for the Service then return every matching Pod's IP directly instead of one shared address — what a StatefulSet uses so each replica (db-0, db-1, ...) is reachable individually rather than load-balanced as an interchangeable group.

yaml
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None
  selector:
    app: db
  ports:
    - port: 5432

With this in place, db-0.db.default.svc.cluster.local resolves to exactly that one Pod — essential for a clustered database where replicas aren’t interchangeable and a client needs to reach a specific one (the primary, say) rather than whichever one a load balancer happened to pick.

ExternalName: an internal DNS alias for something outside the cluster

An ExternalName Service doesn’t proxy traffic or get a virtual IP at all — it's a DNS CNAME, purely so in-cluster code can use the same short, consistent service-name.namespace pattern for something that actually lives outside the cluster, like a managed database.

yaml
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  type: ExternalName
  externalName: prod-db.abc123.us-east-1.rds.amazonaws.com

Code in the cluster connects to db exactly like it would an in-cluster Service; swapping the managed database for an in-cluster one later (or vice versa) is then a one-line change to this Service, not a hunt through every app's configuration.

Checking it from the command line

terminal
kubectl get svc
kubectl get endpoints docktionary-app   # which Pod IPs are actually backing it
kubectl get ingress
kubectl describe ingress docktionary

# Forward a local port straight to a Pod, bypassing the Service entirely
kubectl port-forward deployment/docktionary-app 3000:3000

None of this restricts traffic between Pods inside the cluster -- a Service makes a group of Pods reachable, it doesn’t make anything else unreachable. That's a separate, allow-everything-by- default problem; see NetworkPolicy.