Kubernetes-101: Ingress
Tags: kubernetes
Published: 2023-06-16 10:00:00

In this article we will look at the **Ingress** resource. An Ingress exposes a Service on HTTP or HTTPS outside of the Kubernetes cluster. This sounds suspiciously much like what we already did using only a Service itself. The Ingress can do much more than what we could do with only a Service.

To actually make the Ingress resource work you must have an **Ingress controller** in your cluster. In this article we will use the Nginx Ingress controller in a Minikube cluster.

A schematic view of the role of the Ingress resource is shown in the following image:

![ingress](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eytdd562aga40qn7cvsv.png)

As in the illustration the Ingress resource can configure multiple _routing rules_ that send traffic to different Services in your Kubernetes cluster. The routing rules can look at the hostname and the path to determine where traffic should be sent. The connection between the outside world and the Kubernetes cluster is usually a load balancer of some sort (labeled LB in the figure above), e.g. an Application Load Balancer in AWS, but it does not have to be.

## Ingress controllers

There are a number of Ingress controllers available. There are three Ingress controllers that are officially supported by the Kubernetes project: AWS, GCE, and the Nginx ingress controller.

I am using Minikube on my laptop, so for me it makes sense to use the Nginx Ingress controller. The Nginx Ingress controller uses Nginx behind the scenes to handle the functionality that the Ingress resources provide.

The process of how to install the Nginx Ingress controller on Minikube involves using the `minikube addons` command:

```console
$ minikube addons enable ingress

You can view the list of minikube maintainers at: https://github.com/kubernetes/minikube/blob/master/OWNERS
💡  After the addon is enabled, please run "minikube tunnel" and your ingress resources would be available at "127.0.0.1"
    ▪ Using image k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v1.2.1
    ▪ Using image k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled
```

After running this command we can see that the Nginx Ingress controller is installed in the `ingress-nginx` Namespace:

```console
$ kubernetes get pods -n ingress-nginx

NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-98mcb        0/1     Completed   0          107s
ingress-nginx-admission-patch-rkjj6         0/1     Completed   0          107s
ingress-nginx-controller-5959f988fd-kw6jk   1/1     Running     0          107s
```

The installation process for other Ingress controllers will differ from what I showed you here. In general the process involves installing the controller using Helm. Read the documentation for the Ingress controller that you need to install. A list of some of the available Ingress controllers can be found in the [official documentation](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers).

## Ingress

We now have an Ingress controller installed in our cluster. We can now create Ingress resources that work! Note that if we didn't have an Ingress controller we could still create Ingress resources, but they would not provide any functionality.

A basic Kubernetes manifest for an Ingress looks like this:

```yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-service
                port:
                  number: 80
```

As with most manifests this one has `apiVersion`, `kind`, `metadata`, and `spec`. In `metadata` we see our first example of _annotations_.

An annotation is similar to labels, but are more commonly used to configure settings for resources. In the example above there is one annotation named `nginx.ingress.kubernetes.io/rewrite-target` with the value `/`. This annotation tells the Nginx ingress-controller to rewrite the target path to `/` for incoming requests. This particular annotation only makes sense to the Nginx ingress-controller. There are more advanced things you can do using this annotation, but we keep things simple in this article.

Most of the magic with an Ingress is configured in `.spec.rules` where we have our routing rules. This is where we define what traffic this Ingress resource listens to as well as where it will send this traffic in our Kubernetes cluster. In the example above I have a single rule for the host `example.com`. If I owned this domain and I point it at my Kubernetes cluster, then traffic reaching my cluster would be handled by this Ingress resource. Inside my routing rule I match on a single route, the root route `/`. Traffic destined to `example.com/` will match this routing `rule` and this `path`, and the traffic will be sent to what I define inside of `.spec.rules[0].http.paths[0].backend`. In this case I have specified that the traffic should be sent to a Service named `my-service` on port `80`.

There are a lot more you can do with these routing rules, for instance you can have rules for different hosts and different path prefixes. See the [official documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#types-of-ingress) for some additional examples.

Once we have written down our Kubernetes manifest we can create the Ingress resource using `kubectl apply`:

```console
$ kubectl apply -f ingress.yaml

ingress.networking.k8s.io/my-ingress created
```

We can query our cluster for all Ingress resources with `kubectl get ingresses` (or `kubectl get ingress`):

```console
$ kubectl get ingresses

NAME         CLASS   HOSTS         ADDRESS        PORTS   AGE
my-ingress   nginx   example.com   192.168.49.2   80      32s
```

We could also use the short for for `ingress`/`ingresses` which is `ing`:

```console
$ kubectl get ing

NAME         CLASS   HOSTS         ADDRESS        PORTS   AGE
my-ingress   nginx   example.com   192.168.49.2   80      40s
```

To view details about a specific Ingress resource we can use `kubectl describe`:

```console
$ kubectl describe ingress my-ingress

Name:             my-ingress
Labels:           <none>
Namespace:        default
Address:          192.168.49.2
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host         Path  Backends
  ----         ----  --------
  example.com
               /   my-service:80 (<error: endpoints "my-service" not found>)
Annotations:   nginx.ingress.kubernetes.io/rewrite-target: /
Events:
  Type    Reason  Age                From                      Message
  ----    ------  ----               ----                      -------
  Normal  Sync    25s (x2 over 54s)  nginx-ingress-controller  Scheduled for sync
```

This particular Ingress does not work because I have not created the Service that the Ingress points to, so I will delete it using `kubectl delete`:

```console
$ kubectl delete -f ingress.yaml

ingress.networking.k8s.io "my-ingress" deleted
```

### Sample application

To test the Ingress resource I will first set up a sample application consisting of a Deployment and a Service. The application uses the Nginx image, but do not confuse this instance of Nginx with the Nginx Ingress controller! The full Kubernetes manifests for my application looks like this:

```yaml
# application.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

I create my application using `kubectl apply`:

```console
$ kubectl apply -f application.yaml

deployment.apps/nginx-deployment created
service/nginx-service created
```

### Creating an Ingress resource

Let's now add an Ingress resource:

```yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: hello-world.info
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-service
                port:
                  number: 80
```

- The name of this resource is defined in `.metadata.name` and is set to `nginx-ingress`.
- There is a single annotation added in `.metadata.annotations`, it is named `nginx.ingress.kubernetes.io/rewrite-target` and has the value `/`. This annotations says that the target is always rewritten to `/` no matter what path I try to go to.
- There is a single rule defined in `.spec.rules`. This rule is for the host `hello-world.info`.
- There is a single path defined for the `hello-world.info` rule in `.spec.rules[0].http.paths`, this is for the `/` path. This path will send the traffic to the service named `nginx-service` on port `80`.

To make the host `hello-world.info` work I must either own the domain (I don't!) or I can edit my `/etc/hosts` file to route traffic destined to `hello-world.info` to go to my localhost instead. I add the following line to `/etc/hosts`:

```conf
127.0.0.1 hello-world.info
```

I create my Ingress resource with `kubectl apply`:

```console
$ kubectl apply -f ingress.yaml

ingress.networking.k8s.io/nginx-ingress created
```

After a while I can see that the Ingress resource is ready:

```console
$ kubectl get ing

NAME            CLASS   HOSTS              ADDRESS        PORTS   AGE
nginx-ingress   nginx   hello-world.info   192.168.49.2   80      19s
```

### Verifying that the Ingress works

Now all that is left to do is to test that my Ingress works. Since I am on a Mac and using Minikube there are some additional steps I need to do, as you might remember from an earlier article. I need to tunnel traffic:

```console
$ minikube tunnel

✅  Tunnel successfully started

📌  NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...

❗  The service/ingress nginx-ingress requires privileged ports to be exposed: [80 443]
🔑  sudo permission will be asked for it.
🏃  Starting tunnel for service nginx-ingress.
```

In a separate terminal window I run curl to verify that my Ingress works:

```console
$ curl hello-world.info

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

It works!

## HTTPS traffic with TLS certificate

What we have seen so far in this article is HTTP traffic. In the real world we would rather use HTTPS traffic. You can configure an Ingress to use a TLS certificate to allow it to terminate HTTPS traffic from the outside world. The first step is to add the TLS certificate as a Secret resource in your Kubernetes cluster and then reference it from your ingress resource.

For example, we can define a secret of type `kubernetes.io/tls` with two keys (`tls.crt` and `tls.key`):

```yaml
apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: my-tls-certificate
data:
  tls.crt: <base64 encoded cert>
  tls.key: <base64 encoded key>
```

Then we can create an Ingress resource where we reference the Secret in `.spec.tls[0].secretName` for the corresponding hostname (i.e. the certificate must match the hostname specified in `.spec.tls[*].hosts`):

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
spec:
  tls:
    - hosts:
        - hello-world.info
      secretName: my-tls-certificate # reference to the secret
  rules:
    - host: hello-world.info
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-service
                port:
                  number: 80
```

## Summary

In this article we created and worked with the **Ingress** resource. An Ingress can safely be considered to be a more advanced component of the Kubernetes world, since it often involves external resources such as cloud load balancers and secrets such as TLS certificates.

We have only scratched the surface of what the Ingress resource has to offer, but it is enough to get us started to learn more. We now understand that to work with Ingress resources we must first install an Ingress controller. We know that there are many Ingress controllers to choose from, and what we choose depends on where we run our cluster and what needs we have. We saw concrete examples of using the Nginx controller in a local Minikube cluster. We also saw what it takes to set up an Ingress with a HTTPS-listener.

In the next article I will look at three important concepts related to security in our Kubernetes clusters: **Service Accounts**, **security contexts**, and **NetworkPolicies**. See you there!