Ingress
Ingress is a mechanism to allow the external world some visibility of Services in your Kubernetes cluster.
It is clearly worth a page on its own.
I've been following Craig Johnston at https://imti.co/web-cluster-ingress/.
Overview
Ingress gets a bit (read: a good deal) more complicated. But not that much.
Broadly, the Ingress controller, usually nginx, is given some service descriptions which tell it to map HTTP(S) requests to Services. It even handles multiple HTTP hostnames. Neat!
(Although if you don't use the correct HTTP Host: header your request won't work!)
The one extra component, is a fallback/default handler, called defaultbackend.
Maybe the one difference is that Craig Johnston uses a DaemonSet (rather than a Deployment) which forces the use of an instance of the controller on all (worker) nodes including when you add a new one.
Although I originally followed the example fairly closely (modulo the usual API changes) I did have to update it for the mandatory IngressClass
There's also two broad swathes to this: we need to instantiate the Ingress controller; and then instantiate a Service to use the Ingress.
The Ingress Controller
Several steps here.
NameSpace
# tee 00-namespace.yml | kubectl create -f - apiVersion: v1 kind: Namespace metadata: name: nginx-ingress
Default Backend
Here we have both a Deployment and a Service:
# tee 01-backend.yml | kubectl create -f - apiVersion: apps/v1 kind: Deployment metadata: name: default-http-backend labels: app: default-http-backend namespace: ingress-nginx spec: replicas: 1 selector: matchLabels: app: default-http-backend template: metadata: labels: app: default-http-backend spec: terminationGracePeriodSeconds: 60 containers: - name: default-http-backend # Any image is permissible as long as: # 1. It serves a 404 page at / # 2. It serves 200 on a /healthz endpoint image: gcr.io/google_containers/defaultbackend:1.4 livenessProbe: httpGet: path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 ports: - containerPort: 8080 resources: limits: cpu: 10m memory: 20Mi requests: cpu: 10m memory: 20Mi --- apiVersion: v1 kind: Service metadata: name: default-http-backend namespace: ingress-nginx labels: app: default-http-backend spec: ports: - port: 80 targetPort: 8080 selector: app: default-http-backend
When we try to access things we shouldn't we'll get back an HTTP 404 response, or a default backend - 404 message from curl.
ConfigMaps
Next we need to create three ConfigMaps, one for the nginx configuration and one for each of TCP and UDP services configuration.
nginx ConfigMap
# tee 02-nginx-configmap.yml | kubectl create -f - kind: ConfigMap apiVersion: v1 metadata: name: nginx-configuration namespace: ingress-nginx labels: app: ingress-nginx
TCP/UDP ConfigMaps
# tee 03-tcp-services-configmap.yml | kubectl create -f - kind: ConfigMap apiVersion: v1 metadata: name: tcp-services namespace: ingress-nginx # tee 04-udp-services-configmap.yml | kubectl create -f - kind: ConfigMap apiVersion: v1 metadata: name: udp-services namespace: ingress-nginx
RBAC
There's several concomitant sections here. We create a ServiceAccount to use the rights and both Role and ClusterRole (and therefore RoleBinding and ClusterRoleBindings) settings for in various kinds of manipulations we need to do.
We need to be able to manipulate IngressClasses in addition to Craig's example.
There is some commentary around the resourceNames (being ingress-controller-leader-nginx) for which I have no other information. Magic, er, numbers. ingress-controller-leader appears (at some point) as a ConfigMap though created by whom is a mystery.
# tee 05-rbac.yml | kubectl create -f - apiVersion: v1 kind: ServiceAccount metadata: name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nginx-ingress-clusterrole rules: - apiGroups: - "" resources: - configmaps - endpoints - nodes - pods - secrets verbs: - list - watch - update - apiGroups: - "" resources: - nodes verbs: - get - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - "networking.k8s.io" resources: - ingresses - ingressclasses verbs: - get - list - watch - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - "networking.k8s.io" resources: - ingresses/status verbs: - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: nginx-ingress-role namespace: ingress-nginx rules: - apiGroups: - "" resources: - configmaps - pods - secrets - namespaces verbs: - get - apiGroups: - "" resources: - configmaps resourceNames: # Defaults to "<election-id>-<ingress-class>" # Here: "<ingress-controller-leader>-<nginx>" # This has to be adapted if you change either parameter # when launching the nginx-ingress-controller. - "ingress-controller-leader-nginx" verbs: - get - update - apiGroups: - "" resources: - configmaps verbs: - create - apiGroups: - "" resources: - endpoints verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: nginx-ingress-role-nisa-binding namespace: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: nginx-ingress-role subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nginx-ingress-clusterrole-nisa-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nginx-ingress-clusterrole subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx
DaemonSet
This didn't work out of the box and I had to rummage around a bit to get a working (or workable) image, here using k8s.gcr.io/ingress-nginx/controller:v1.0.5 rather than the one from quay.io.
I think that that is what required the use of the extra argument --controller-class=example.com/ingress-nginx1 where that controller class will be used in a moment.
You can see the use of the ConfigMaps although I don't see any actual usage (kubectl -n ingress-nginx describe configmap/X)
# tee 06-ds.yml | kubectl create -f - apiVersion: apps/v1 kind: DaemonSet metadata: name: nginx-ingress-controller namespace: ingress-nginx spec: selector: matchLabels: app: ingress-nginx template: metadata: labels: app: ingress-nginx annotations: prometheus.io/port: '10254' prometheus.io/scrape: 'true' spec: serviceAccountName: nginx-ingress-serviceaccount hostNetwork: true containers: - name: nginx-ingress-controller #image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.14.0 image: k8s.gcr.io/ingress-nginx/controller:v1.0.5 args: - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/nginx-configuration - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services - --udp-services-configmap=$(POD_NAMESPACE)/udp-services - --annotations-prefix=nginx.ingress.kubernetes.io - --controller-class=example.com/ingress-nginx1 env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ports: - name: http containerPort: 80 hostPort: 80 - name: https containerPort: 443 hostPort: 443 livenessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 readinessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10254 scheme: HTTP periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 securityContext: runAsNonRoot: false
Service
We can create a NodePort Service to front-up the DaemonSet (much like we would have done with a Deployment).
Notice that we're grabbing both port 80 and 443 on all nodes for our Service and redirecting them to the ingress-nginx app, our ingress controller.
# tee 07-service.yml | kubectl create -f - apiVersion: v1 kind: Service metadata: name: ingress-nginx namespace: ingress-nginx spec: type: NodePort ports: - name: http port: 80 targetPort: 80 protocol: TCP - name: https port: 443 targetPort: 443 protocol: TCP selector: app: ingress-nginx
IngressClass
We need to create an IngressClass to satisfy the latest code's demands which appears to transform the --controller-class name we gave the DaemonSet into an ingress-nginx-one IngressClass name.
We'll use the IngressClass name when we add new Ingresses later.
# tee 08-ingressclass.yml | kubectl create -f - apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: labels: app.kubernetes.io/component: controller name: ingress-nginx-one annotations: ingressclass.kubernetes.io/is-default-class: "true" spec: controller: example.com/ingress-nginx1
Testing
We should be able to test the basic Ingress controller Service at this point:
# kubectl -n ingress-nginx get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default-http-backend ClusterIP 10.106.29.189 <none> 80/TCP 10d ingress-nginx NodePort 10.101.162.89 <none> 80:32568/TCP,443:30713/TCP 10d # curl -s 10.101.162.89 default backend - 404
Good.
The Ingress Service
You can use Craig's txn2/ok service or we can use our Status Server which also tests we can use our private docker registry. Or both!
Deployment
Here, we're back to being a regular Kubernetes user, no special privileges.
Notice that we have to pass imagePullSecrets to allow containerd authorisation to pull from our private docker registry.
Also note that just because we pass, here, reg-cred-secret doesn't mean that that docker-registry Secret exists. Particularly as, when we were creating the private docker registry, we were doing everything as admin not our local, less privileged, User account and in a different NameSpace.
Let's assume our User can use the NameSpace my-namespace.
$ tee 10-status-deployment.yml | kubectl -n my-namespace create -f - apiVersion: apps/v1 kind: Deployment metadata: name: status labels: app: status system: test spec: replicas: 1 selector: matchLabels: app: status template: metadata: labels: app: status system: example spec: containers: - name: status image: docker-registry:5000/example.com/app/status imagePullPolicy: Always env: - name: IP value: "0.0.0.0" - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: SERVICE_ACCOUNT valueFrom: fieldRef: fieldPath: spec.serviceAccountName ports: - name: status-port containerPort: 8080 imagePullSecrets: - name: reg-cred-secret
Service
$ tee 11-status-service.yml | kubectl -n my-namespace create -f - apiVersion: v1 kind: Service metadata: name: status labels: app: status system: test spec: selector: app: status ports: - protocol: "TCP" port: 8080 targetPort: 8080 type: NodePort
Test
We can quickly test the service is working at all:
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ... status NodePort 10.97.111.97 <none> 8080:31214/TCP 21h $ curl -s 10.97.111.97:8080 {"call-uuid":"2af3dd00-bc82-4cde-8821-a3cbd4b6eeb4","client-ip":"10.254.42.128","count":1,"node-name":"k8s-w2","pod-ip":"0.0.0.0","pod-name":"status-5fb664cbf6-jk6d5","pod-namespace":"my-namespace","pod-port":"8080","service-account":"default","svc-uuid":"97746796-205a-4f6f-8435-81c18205cfc5","time":"2022-04-03T15:41:49.489708544Z"}
Ingress
The exciting bit!
$ tee 12-status-ingress.yml | kubectl -n my-namespace create -f - apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: status labels: app: status system: test spec: ingressClassName: ingress-nginx-one rules: - host: k8s-ingress.example.com http: paths: - backend: service: name: status port: number: 8080 path: /status pathType: Prefix
where we specify the IngressClassName, ingress-nginx-one and two important attributes:
the host expected in the HTTP Host: header
This implies we must finagle the DNS to "make it so."
the path prefix for the URL
In other words, the use of http://k8s-ingress.example.com/status will be redirected to our status microservice listening on port 8080.
Obviously, we should be using some sort of competent external-to-Kubernetes load balancing but, here we are.
We can get some configuration output with the describe verb:
$ kubectl describe ingress/status Name: status Labels: app=status system=test Namespace: my-namespace Address: 172.18.0.189,172.18.0.244 Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" is forbidden: User "me" cannot get resource "endpoints" in API group "" in the namespace "kube-system">) Rules: Host Path Backends ---- ---- -------- k8s-ingress.example.com /status status:8080 (10.254.46.11:8080) ...
The IP address, 10.254.46.11 is that of the Pod running the microservice.
Checks
We can check where we are:
$ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE status ingress-nginx-one k8s-m1.example.com 172.18.0.189,172.18.0.244 80 22h
Where the two IP addresses are those of my worker nodes.
Assuming we have finagled the DNS so that k8s-ingress.example.com points at our two worker nodes we should be able to:
$ curl k8s-ingress.office.soho/status Hello World from /status
Eh?
Hmm, it turns out that Ingress is not doing any rewriting (can it?) so our request for /status is being passed verbatim to our microservice. It so happens that, out of interest, we covered the /:path GET variant and it will respond with Hello World from $path. So fair enough.
The correct solution is to edit either the YAML and replace path: /status to path: / or kubectl edit ingress/status.
If you were also running Craig's txn2/ok Service then there'll be a clash between both Ingresses claiming /.
Testing
$ curl k8s-ingress.office.soho/ {"call-uuid":"eb0f4205-dfee-477d-9959-12cccb6812ad","client-ip":"172.18.0.244","count":2,"node-name":"k8s-w2","pod-ip":"0.0.0.0","pod-name":"status-5fb664cbf6-jk6d5","pod-namespace":"my-namespace","pod-port":"8080","service-account":"default","svc-uuid":"97746796-205a-4f6f-8435-81c18205cfc5","time":"2022-04-03T17:31:56.83909024Z"}
Looks OK.
We can test our other microservcie routes:
$ curl k8s-ingress.office.soho/status Hello World from /status $ curl k8s-ingress.office.soho/secret/sauce /secret/sauce Not Found
Document Actions