Ingress Exposure

TinySystems components can expose HTTP endpoints to the internet via Kubernetes Ingress. This guide covers ingress configuration and common patterns.

Overview

+-----------------------------------------------------------------------------+
|                         INGRESS EXPOSURE                                     |
+-----------------------------------------------------------------------------+

  Internet                 Ingress Controller              Module Pods
      |                          |                             |
      |  https://api.example.com |                             |
      |  ----------------------> |                             |
      |                          |                             |
      |                          |  Route to Service           |
      |                          |  ----------------------->   |
      |                          |                             |
      |                          |           Load Balance      |
      |                          |  +------------------------->| Pod 1
      |                          |  |                          |
      |                          |  +------------------------->| Pod 2
      |                          |  |                          |
      |                          |  +------------------------->| Pod 3
      |                          |                             |

Basic Ingress Setup

Exposing a Port

Port exposure is provided by the SDK's resource manager, delivered to components through the module.ClientAware capability. The signature is:

ExposePort(ctx context.Context, autoHostName string, hostnames []string, port int) ([]string, error)
DisclosePort(ctx context.Context, port int) error
type Server struct {
    client module.Client // ExposePort / DisclosePort
}

// OnClient is called once by the framework during node update.
func (s *Server) OnClient(c module.K8sClient) {
    if pc, ok := c.(module.Client); ok {
        s.client = pc
    }
}

func (s *Server) exposeHTTP(ctx context.Context) error {
    hostnames, err := s.client.ExposePort(ctx, "my-api", []string{"api.example.com"}, s.currentPort)
    if err != nil {
        return err
    }
    // hostnames contains the final list, including any auto-generated hostname
    return nil
}

ExposePort does not create new Kubernetes objects. It locates the module release's existing Service and Ingress (by the Helm release labels) and mutates them:

  • adds a port<N> entry to the Service
  • appends one Ingress rule per hostname (path /, pathType: Prefix) pointing at the Service
  • appends one TLS entry per new hostname with secret name <hostname>-tls

Resulting Changes

Service (module release Service, port appended):

spec:
  ports:
    # existing ports ...
    - name: port8080
      port: 8080
      targetPort: 8080

Ingress (module release Ingress, rule and TLS entry appended):

spec:
  tls:
    - hosts:
        - api.example.com
      secretName: api.example.com-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: <module-release-service>
                port:
                  number: 8080

Automatic Hostnames

When the release Ingress carries the tinysystems.io/ingress-hostname-suffix annotation, passing a non-empty autoHostName appends an extra hostname <autoHostName>-<suffix>:

// With annotation tinysystems.io/ingress-hostname-suffix: "playground.example.com"
hostnames, _ := s.client.ExposePort(ctx, "my-api", nil, 8080)
// hostnames == []string{"my-api-playground.example.com"}

At least one of autoHostName / hostnames must be provided, otherwise ExposePort returns an error.

TLS Configuration

TLS is not optional: every new hostname gets a TLS entry with secret name <hostname>-tls. If cert-manager is configured for the release Ingress (via its annotations), certificates are provisioned automatically. There is no parameter for a custom certificate secret — to use an existing certificate, manage the Ingress manually via the raw Kubernetes client (module.K8sClient).

Multiple Hostnames

Expose the same port on multiple domains:

hostnames, err := s.client.ExposePort(ctx, "", []string{
    "api.example.com",
    "api.example.org",
    "api.example.net",
}, 8080)

Each hostname gets its own rule and its own TLS entry/secret:

spec:
  tls:
    - hosts: [api.example.com]
      secretName: api.example.com-tls
    - hosts: [api.example.org]
      secretName: api.example.org-tls
    - hosts: [api.example.net]
      secretName: api.example.net-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: <module-release-service>, port: { number: 8080 } } }
    - host: api.example.org
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: <module-release-service>, port: { number: 8080 } } }
    - host: api.example.net
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: <module-release-service>, port: { number: 8080 } } }

Removing Exposure

DisclosePort reverses ExposePort: it removes the port from the Service and drops every Ingress rule (and associated TLS entry) that points at it:

if err := s.client.DisclosePort(ctx, 8080); err != nil {
    return err
}

Path-Based Routing

Route different paths to different components:

# Manual ingress for advanced routing
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-path-ingress
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /users
            pathType: Prefix
            backend:
              service:
                name: user-service
                port:
                  number: 8080
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 8080

Ingress Annotations

NGINX Ingress Controller

metadata:
  annotations:
    # Timeouts
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"

    # Body size
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"

    # WebSocket support
    nginx.ingress.kubernetes.io/websocket-services: "<module-release-service>"

    # Rate limiting
    nginx.ingress.kubernetes.io/limit-rps: "100"

    # CORS
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "*"

Where Annotations Live

ExposePort takes no annotations parameter. Annotations belong to the module release's Ingress (set via the Helm chart values) and apply to every hostname exposed through it. A component that needs per-endpoint annotations must manage its own Ingress object using the raw Kubernetes client delivered via module.ClientAware (GetK8sClient()).

WebSocket Support

Enable WebSocket upgrade:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
    nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

Health Checks

Readiness Probe

Ensure pods are ready before receiving traffic:

# In module Helm chart
spec:
  containers:
    - name: module
      readinessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10

Backend Health

NGINX ingress checks backend health:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/upstream-vhost: "api.example.com"
    nginx.ingress.kubernetes.io/health-check-path: "/healthz"

Load Balancing

Default (Round Robin)

Kubernetes Service distributes traffic evenly.

Session Affinity

Sticky sessions for stateful applications:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "route"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"

Monitoring Ingress

Check Ingress Status

kubectl get ingress -n tinysystems
kubectl describe ingress -n tinysystems -l app.kubernetes.io/name=tinysystems-operator

View Logs

# NGINX Ingress Controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

Metrics

# Prometheus metrics from ingress controller
curl http://ingress-controller:10254/metrics

Troubleshooting

Certificate Not Ready

# Check cert-manager (secret/certificate is named <hostname>-tls)
kubectl get certificates -n tinysystems
kubectl describe certificate api.example.com-tls -n tinysystems

# Check challenges
kubectl get challenges -n tinysystems

502 Bad Gateway

  • Check pod readiness
  • Verify service selector matches pods
  • Check pod logs for errors
kubectl get pods -n tinysystems -l app=http-module
kubectl logs -n tinysystems -l app=http-module

DNS Not Resolving

  • Verify DNS record exists
  • Check ingress host configuration
  • Ensure ingress controller has external IP
kubectl get svc -n ingress-nginx
nslookup api.example.com

Best Practices

1. Expose from the Leader Only

ExposePort mutates shared Service/Ingress objects. In multi-replica modules, gate it with utils.IsLeader(ctx) so only one pod performs the mutation.

2. Configure Timeouts

Match timeouts to your processing time:

nginx.ingress.kubernetes.io/proxy-read-timeout: "300"  # 5 minutes for long operations

3. Enable Rate Limiting

Protect against abuse:

nginx.ingress.kubernetes.io/limit-rps: "50"
nginx.ingress.kubernetes.io/limit-connections: "10"

4. Monitor Certificate Expiry

Set up alerts for certificate expiration:

# Prometheus rule
- alert: CertificateExpiringSoon
  expr: certmanager_certificate_expiration_timestamp_seconds - time() < 604800
  for: 1h

Next Steps