Home pet cluster. Kubernetes on CoreOS. Part 3: Ingress
My Kubernetes is up and running, and I’ve decided to expose certain services to the Internet, while keeping other services inside the home network.
10000ft Overview
From the high level perspective it looks like this:
- External DNS configuration to point to the ISP-owned IP of my router (either using static IP or via DynDNS).
- Router configuration, one of the following:
- OpenWRT router has HAProxy 2, which is configured to route clients supporting SNI either to K8s or to other targets (this is my case).
- Simple port forwarding to route TCP traffic on port
443
to proper destination.
- K8s
LoadBalancer
(MetalLB) that would propagate custom IP address to the router. This allows scheduling LB on different nodes and failover. - External and Internal Ingress Controller (Nginx) that would take care of k8s
Ingress
objects and createLoadBalancer
services via MetalLB. - OAuth2 Proxy configured in Nginx Ingress as external auth (for externally exposed services)
- K8s CoreDNS pointing to router DNS instead of Google DNS to allow inter-service communication via LAN (useful for Nginx <-> OAuth2 Proxy, for example)
- Cert Manager requesting SSL certificates that are used by Ingress Controller
User request (assuming HTTPS traffic to K8s service using client supporting SNI) originating from the Internet is routed via:
- External DNS pointing to the router IP
- HAProxy 2
- LAN IP address associated with external MetalLB
LoadBalancer
associated with MAC address of one of the nodes network card -
kube-proxy
spreading traffic between one ofmetallb-speaker
s - Nginx with proper SSL certificates performs SSL termination
- Nginx performs authentication via OAuth2 Proxy (K8s CoreDNS uses OpenWRT DNS to lookup the IP address, and then request is forwarded (again) via MetalLB and nginx)
- Service and Pod networking do their magic
- HTTP traffic finally reaches the pod
OpenWRT
HAProxy
OpenWRT router is configured with HAProxy 2 listening on port 443
with SNI configuration allowing to proxy certain domain names to Kubernetes, while other to another target.
If Kubernetes (on a single IP) is the only destination, simple port forwarding can be used instead.
Here’s snippet of HAProxy config, including web UI and prometheus metrics:
global
daemon
defaults
timeout client 30s
timeout server 30s
timeout connect 5s
frontend ft_ssl
bind :443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
acl k8s_app_1 req_ssl_sni -i app1.k8s.example.com
acl k8s_app_2 req_ssl_sni -i app2.k8s.example.com
use_backend bk_ssl_k8s_x64 if k8s_app_1
use_backend bk_ssl_k8s_x64 if k8s_app_2
default_backend bk_ssl_non_k8s
# K8s x64
backend bk_ssl_k8s_x64
mode tcp
balance roundrobin
stick-table type binary len 32 size 30k expire 30m
acl clienthello req_ssl_hello_type 1
acl serverhello rep_ssl_hello_type 2
tcp-request inspect-delay 5s
tcp-request content accept if clienthello
tcp-response content accept if serverhello
stick on payload_lv(43,1) if clienthello
stick store-response payload_lv(43,1) if serverhello
option ssl-hello-chk
server k8s-x64-node1 192.168.0.3:443 check
#server k8s-x64-node2 192.168.0.4:443 check
backend bk_ssl_non_k8s
mode tcp
balance roundrobin
stick-table type binary len 32 size 30k expire 30m
acl clienthello req_ssl_hello_type 1
acl serverhello rep_ssl_hello_type 2
tcp-request inspect-delay 5s
tcp-request content accept if clienthello
tcp-response content accept if serverhello
stick on payload_lv(43,1) if clienthello
stick store-response payload_lv(43,1) if serverhello
option ssl-hello-chk
server non_k8s 192.168.0.2:443
listen stats
bind *:1936
http-request use-service prometheus-exporter if { path /metrics }
mode http
stats enable
stats uri /
stats refresh 10s
Internal DNS
Router’s DNS is used to avoid routing traffic with both source and destination within the LAN.
It maps hostnames to the IP addresses of Kubernetes ingress services (type: LoadBalancer
).
uci add dhcp domain
uci set dhcp.@domain[-1].name='app1.k8s.example.com'
uci set dhcp.@domain[-1].ip='192.168.0.3'
uci add dhcp domain
uci set dhcp.@domain[-1].name='app2.k8s.example.com'
uci set dhcp.@domain[-1].ip='192.168.0.3'
uci commit
luci-reload
service dnsmasq restart
Kubernetes
I was not a huge fan of Helm 2, especially due to the Tiller component. But also due to many charts using outdated apiVersion
in their templates, but now as K8s API became much more mature, I am enjoying Helm 3 instead. That’s why most of the services in this post are deployed via Helm.
Kubespray config
Several Kubespray configuration options have to be adjusted to make this setup working.
One is related to configure Kubernetes DNS to use OpenWRT DNS as an upstream instead of Google DNS.
Another one is about enabling scheduling on a master node of my two-node cluster.
Also, I used this as a chance to convert inventory to YAML, which allows me to configure node labels :)
Additionally, I’ve configured several components to expose their metrics in Prometheus format.
Inventory:
all:
hosts:
nuc:
ansible_host: 192.168.0.10
ansible_user: core
etcd_member_name: nuc5ppyh
node_labels: {'disktype': 'hdd', 'cputype': 'pentium'}
udoo:
ansible_host: 192.168.0.11
ansible_user: core
node_labels: {'disktype': 'ssd', 'cputype': 'celeron'}
children:
etcd: {hosts: {nuc: {}}}
k8s-cluster:
children:
kube-master: {hosts: {nuc: {}}}
# Master node must be included into kube-node to avoid NoSchedule taint
kube-node: {hosts: {nuc: {}, udoo: {}}}
Configuration:
## Upstream dns servers
upstream_dns_servers:
- 192.168.0.1
# - 8.8.8.8
# - 8.8.4.4
## Prometheus metrics
# The IP address and port for the metrics server to serve on
# (set to 0.0.0.0 for all IPv4 interfaces and `::` for all IPv6 interfaces)
kube_proxy_metrics_bind_address: 0.0.0.0:10249
calico_felix_prometheusmetricsenabled: true
calico_felix_prometheusmetricsport: 9091
calico_felix_prometheusgometricsenabled: true
calico_felix_prometheusprocessmetricsenabled: true
Here’s quick way to verify that changes were correctly applied:
# Checking labels
kubectl get node --show-labels
# Checking that NoSchedule was removed from master
kubectl get nodes -o json | jq '.items[].spec.taints'
# Checking that K8s DNS uses local DNS as an upstream
kubectl -nkube-system get cm coredns -oyaml | grep upstream
kubectl -nkube-system get cm nodelocaldns -oyaml | grep forward
# Checking that Prometheus metrics are accessible
kubectl -n kube-system get cm kube-proxy -oyaml | grep metricsBindAddress
curl 192.168.0.10:9091/metrics
curl 192.168.0.11:9091/metrics
MetalLB
MetalLB is an awesome little load balancer implementation for both enthusiasts (like myself) and serious production deployments running on bare metal.
I neither have fancy hardware to support BGP, nor actually need it.
Layer 2 (ARP) mode and proxying all the traffic using single node (with an automatic failover, thanks to ARP!) works for me!
Without further ado, here’s my Helm 3 configuration for MetalLB. It matches IP addresses that I use for OpenWRT internal DNS, as well as for HAProxy configuration. That’s the key
configInline:
address-pools:
# Used in HAProxy and internal DNS, for services accessible from Internet
- name: external
protocol: layer2
addresses:
- 192.168.0.3/32
# Used only in internal DNS, for services accessible from LAN only
- name: internal
protocol: layer2
addresses:
- 192.168.0.5/32
# Of course, Prometheus!
prometheus:
serviceMonitor:
enabled: true
prometheusRule:
enabled: true
Nginx Ingress Controller
Nginx Ingress Controller have to be configured to create proper LoadBalancer
service, and Ingress
objects must map to proper ingress controller and include Authn configuration.
Here’s Helm configuration for external Nginx Ingress (keep an eye on MetalLB annotation!):
controller:
name: controller-external
electionID: ingress-controller-external-leader
ingressClass: nginx-external
kind: DaemonSet
service:
omitClusterIP: true
annotations:
metallb.universe.tf/address-pool: external
metrics:
enabled: true
service:
omitClusterIP: true
defaultBackend:
service:
omitClusterIP: true
Here’s sample Ingress
object leveraging Nginx Ingress external OAuth support and SSL certificates managed via Cert Manager:
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: app1
namespace: app1
annotations:
kubernetes.io/ingress.class: "nginx-external"
nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri"
spec:
tls:
- hosts:
- app1.k8s.example.com
secretName: tls-app1
rules:
- host: app1.k8s.example.com
http:
paths:
- backend:
serviceName: app1
servicePort: 80
path: /
OAuth2 Proxy
OAuth2 Proxy is another wonderful service transparently enabling OAuth2 for applications that don’t support it natively. It works great with Nginx Ingress too.
Helm configuration:
config:
clientID: "app1-cid"
clientSecret: "app1-csec"
# openssl rand -base64 32 | head -c 32
cookieSecret: "cookieMonsterAteTheSecret"
configFile: |-
provider = "foooo"
upstream = "file:///dev/null"
footer = "-"
ingress:
enabled: true
path: /oauth2
hosts:
- app1.k8s.example.com
annotations:
kubernetes.io/ingress.class: "nginx-external"
tls:
- secretName: tls-app1
hosts:
- app1.k8s.example.com
Conclusion
That’s about it, folks. Now I can easily and (more or less) securely access my k8s services from both LAN and Internet using the same host names, proper SSL certificates and OAuth 2 support.