Using Kubernetes Ingress for non-K8 Backends

TL;DR – Make sure you name your ports when you create external endpoints.

In my home environment, I need a reverse proxy that serves all port 80 and 443 requests and can interface easily with LetsEncrypt to ensure all those endpoints are secure. Originally I’ve been using Docker and Jwilder’s nginx proxy to support all these. As it’s just using nginx, you can use it to send stuff to backends that aren’t in docker pretty easily (like the few physical things that aren’t in docker). However, I’ve been transitioning over to Kubernetes and need a similar way to have a single endpoint on those ports that all services can use.

Well, the good news is that the the internet is awash of articles about this. However, after attempting to implement any of them, I was consistently getting 502 errors – no live upstreams. This was happening on a Ubuntu 20.04 LTS system running microk8s v1.19.5.

My original endpoint, service, and ingress configs were the following:

apiVersion: v1
kind: Endpoints
metadata:
  name: external-service
subsets:
  - addresses:
      - ip: <<IP>>
    ports:
      - port: <<PORT>>
        protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
    - name: https
      protocol: TCP
      port: <<PORT>>
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: external-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    cert-manager.io/cluster-issuer: letsencrypt-prod
    cert-manager.io/acme-challenge-type: http01
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
  tls:
  - hosts:
    - external.rebelpeon.com
    secretName: external-prod
  rules:                           
  - host: external.rebelpeon.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: external-service
            port: 
              number: <<PORT>>

This yaml deployed successfully, but as mentioned did not work. With it deployed, when describing the Endpoint:

$ kubectl describe endpoints -n test
Name:         external-service
Namespace:    test
Labels:       <none>
Annotations:  <none>
Subsets:
  Addresses:          <<IP>>
  NotReadyAddresses:  <<none>
  Ports:
    Name     Port  Protocol
    ----     ----  --------
    <unset>  443   TCP

Events:  <none>

When describing the service:

$ kubectl describe services -n test
Name:              external-service
Namespace:         test
Labels:            <none>
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP Families:       <none>
IP:                10.152.183.182
IPs:               <none>
Port:              https  443/TCP
TargetPort:        443/TCP
Endpoints:
Session Affinity:  None
Events:            <none>

Wait a minute, the service lists the endpoints as being blank – not undefined or properly defined as others. When I describe the endpoint of a working K8-managed endpoint, I see that the port has a name, and that’s the only difference.

$ kubectl describe endpoints -n test
Name:         external-service
Namespace:    test
Labels:       <none>
Annotations:  <none>
Subsets:
  Addresses:          <<IP>>
  NotReadyAddresses:  <none>
  Ports:
    Name   Port  Protocol
    ----   ----  --------
    https  443   TCP

So, I changed my config to the following (one line change):

apiVersion: v1
kind: Endpoints
metadata:
  name: external-service
subsets:
  - addresses:
      - ip: <<IP>>
    ports:
      - port: <<PORT>>
        protocol: TCP
        name: https
---
apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
    - name: https
      protocol: TCP
      port: <<PORT>>
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: external-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    cert-manager.io/cluster-issuer: letsencrypt-prod
    cert-manager.io/acme-challenge-type: http01
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
  tls:
  - hosts:
    - external.rebelpeon.com
    secretName: external-prod
  rules:                           
  - host: external.rebelpeon.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: external-service
            port: 
              number: <<PORT>>

And, tada everything works! I can now access physical hosts outside of K8 via the K8 ingress! Sadly, that took about 4 hours of head bashing-in to realize…