---
title: How To Deploy a Frontend on Kubernetes?
url: https://blog.octoperf.com/how-to-deploy-a-frontend-on-kubernetes/
date: '2019-09-05'
authors:
- gpereira
categories:
- DevOps
tags:
- Kubernetes
- Angular
- Proxy
description: A step by step guide to deploying an Angular frontend behind an NGinx Ingress on Kubernetes. K8S installation, Ingress controller deployment and frontend application proxying.
---

# How To Deploy a Frontend on Kubernetes?

> Kubernetes (commonly referred to as "K8s") is an open source system that aims to provide a platform for automating the deployment, scalability and implementation of application containers on server clusters.
> It works with a variety of container technologies, and is often used with Docker.
> It was originally designed by Google and then offered to the Cloud Native Computing Foundation.

This blog post belongs to a series that describe how to use [Minikube](https://github.com/kubernetes/minikube), declarative configuration files and the `kubectl` command-line tool to deploy Docker micro-services on Kubernetes. 
It focuses on **the installation of an [Angular 8 frontend application](#deploy-an-angular-8-frontend) served by an [NGinx Ingress controller](#install-and-configure-nginx-ingress)**.

While being the most complex kind of [Kubernetes object management](https://kubernetes.io/docs/concepts/overview/working-with-objects/object-management/),
the declarative object configuration allows to apply and revert configuration updates.
Also, configuration files can easily be shared or saved into a version control system like Git.

But before going to the [configuration of our frontend](#deploy-an-angular-8-frontend) and its [proxy](#install-and-configure-nginx-ingress), let's see what is needed [in order to follow this tutorial](#prerequisites).


## Prerequisites

Executing this blog post code and configuration samples on a local Linux machine requires:

- [Kubernetes](#install-kubernetes-command-line-client),
- [Minikube](#install-minikube),
- [KVM or VirtualBox](#kvm-and-minikube-driver).

A [basic knowledge of K8s concepts](#k8s-definitions) is also recommended. 

### Install Kubernetes Command Line Client

First, we need to [install Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-on-linux). 
I used the version 1.15:

```shell
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.15.0/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
```

The `kubectl version` command should display the 1.15 version.

> __Tip:__ If you think typing the `kubectl` command is too long, you can easily create a shortcut with a symlink.
> For instance, create this link `sudo ln -s /usr/local/bin/kubectl /usr/local/bin/kctl` to type `kctl` instead of `kubectl`.

### Install Minikube

You can deploy Kubernetes on your machine, but its preferable to do it in a VM when developing. 
It's easier to restart from scratch if you made a mistake, or try several configurations without being impacted by remaining objects.
**Minikube is the tool to test Kubernetes or develop locally.**

Download and [install Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/):

```shell
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
  && chmod +x minikube
sudo install minikube /usr/local/bin
```

### KVM and Minikube driver

**Minikube wraps Kubernetes in a Virtual Machine, so it needs an hypervisor: VirtualBox or KVM.**
Since I do my tests on a small portable computer, I prefer to use the lighter virtualization solution: KVM.
Follow [this guide](https://doc.ubuntu-fr.org/kvm) to install it on Ubuntu.

Finally, install the KVM driver: 

```shell
curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-kvm2
chmod +x docker-machine-driver-kvm2
sudo mv docker-machine-driver-kvm2 /usr/local/bin/
```

Then, launch Minikube using the KVM2 driver:

```shell
> minikube start --vm-driver=kvm2
😄  minikube v1.2.0 on linux (amd64)
🔥  Creating kvm2 VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
🐳  Configuring environment for Kubernetes v1.15.0 on Docker 18.09.6
🚜  Pulling images ...
🚀  Launching Kubernetes ... 
⌛  Verifying: apiserver proxy etcd scheduler controller dns
🏄  Done! kubectl is now configured to use "minikube"
```

### K8S Definitions

Before continuing, it is best to define some concepts that are unique to Kubernetes.
If you are familiar with this solution, you can go directly to [the next chapter](#install-and-configure-nginx-ingress).

![K8S Cluster](https://blog.octoperf.com/img/blog/kubernetes-ingress-nginx-frontend/k8s-cluster.png "K8S Cluster")

- **A K8s cluster is divided in [Nodes](https://kubernetes.io/docs/concepts/architecture/nodes/).** A node is a worker machine and may be a VM or physical machine. 
    In our case, the K8s cluster is composed of a single Node: the Minikube VM. 
- When creating an application [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), **K8s creates one or several [Pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/)** on the available nodes.
    A Pod is a group of one or more Docker containers, with shared storage/network, and a specification for how to run the containers. 
- **Theses Pods are regrouped in [Services](https://kubernetes.io/docs/concepts/services-networking/service/).** 
    A Service defines a policy by which to access its targeted pods.
    For example:
    - A service with the type NodePort exposes the Service on each Node's IP at a static port. 
        From outside the cluster, the service is accessible by requesting `<NodeIP>:<NodePort>`.
    - A LoadBalancer Service exposes the Service externally using the load-balancer of a cloud provider. 

The NodePort solution would work for testing purposes but is not reliable in a production environment.
And the LoadBalancer works only in the Cloud, not in a local test environment.

**The solution that fits any use case is to [install an Ingress Controller and use Ingress rules](#install-and-configure-nginx-ingress).**

## TL; DR

Download and extract the [frontend.zip](https://blog.octoperf.com/img/blog/kubernetes-ingress-nginx-frontend/frontend.zip) archive.

It contains several K8s configuration files for Ingress and the Angular frontend.

It also contains a [Makefile](https://en.wikipedia.org/wiki/Makefile). Here is an extract of this file:

```make
start:
	minikube start --vm-driver=kvm2 --extra-config=apiserver.service-node-port-range=1-30000

mount:
	minikube mount ${PWD}/grafana/config:/grafana

all:
	kubectl apply -R -f .

list:
	minikube service list

watch:
	kubectl get pods -A --watch
```

To launch the complete stack:

1. Run `make start` to start Minikube (or copy paste the command above in a terminal if you do not have `make` on your computer),
3. Execute `make all` to launch the Ingress controller and the Frontend,
4. Wait for the various Pods to start (it may take some time to download the Docker images) using `make watch`,
5. List the available services with `make list`.

```shell
|---------------|----------------------|--------------------------------|
|   NAMESPACE   |         NAME         |              URL               |
|---------------|----------------------|--------------------------------|
| default       | kubernetes           | No node port                   |
| ingress-nginx | ingress-nginx        | http://192.168.39.146:80       |
|               |                      | http://192.168.39.146:443      |
| kube-system   | kube-dns             | No node port                   |
| kube-system   | kubernetes-dashboard | No node port                   |
|---------------|----------------------|--------------------------------|
```

Open the URL of the `ingress-nginx` service with `/administration` appended: http://192.168.39.146:80/administration and [check that the frontend is running and served by the NGINX proxy](#testing-the-installation).

## Install and Configure NGinx Ingress

The concept of Ingress is split in two parts:

- The [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/), it's some kind of wrapper for an HTTP proxy,
- Ingress resources/rules that expose HTTP and HTTPS routes from outside the cluster to services within the cluster, depending on traffic rules.

### Motivation

We already [use HAProxy to redirect the traffic to a specific Docker container](https://blog.octoperf.com/angular-workspaces-multi-application-projects/#serving-production-images-with-haproxy).
It's the same principle here, except that you don't configure the proxy directly but specify Ingress configuration objects.
The proxy configuration is automatically updated for you by the controller.

**There is one issue with Ingress though, the configuration is done using annotation that are specific to the underlying controller.**
So, unfortunately you cannot change the controller implementation without updating the Ingress resources.

The simpler Ingress controller is the one maintained by Kubernetes: [Kubernetes NGinx](https://kubernetes.github.io/ingress-nginx/deploy/), not to be confused with [the one maintained by the NGINX team](https://github.com/nginxinc/kubernetes-ingress).

> __Note:__ I also tried to use [HAproxy's Ingress Controller](https://www.haproxy.com/blog/dissecting-the-haproxy-kubernetes-ingress-controller/) without success.
> I could not configure the URL Rewrite on this one.

Using a proxy and an Ingress controller allows us to serve multiple applications on the same hostname and port (80/443) but with different paths.

### Ingress Controller Installation

**Let's get our hands dirty and [install an NGINX ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress/).**

We use the [`kubectl apply` command](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/#how-to-create-objects).
It creates all resources defined in the given file.
This file can be on the local file system or accessed using a URL. 

> ___Note:__ Skip directly to the next chapter if you want to [expose your Ingress Controller on port 80](#expose-nodeport-80).

So, the first command to execute automatically installs all components required on the K8s cluster:

```shell
> kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
namespace/ingress-nginx created
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount created
clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role created
rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created
```

You can check what is being installed in details by downloading the configuration file.

All the related resources are deployed in a dedicated namespace called `ingress-nginx`.
Check that the NGinx pod is started with the following command (press `CTRL + C` when the container status is _Running_):

```shell
> kubectl get pods -n ingress-nginx --watch
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-7995bd9c47-k2wps   1/1     Running   0          2m6s
```

Our Ingress Controller is started, but not yet accessible externally from our K8s cluster.
**We need to create a NodePort Service to expose it to the outside world.**

Run the following command to apply the configuration provided by the ingress-nginx project to create a NodePort service: 

```shell
> kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/service-nodeport.yaml
service/ingress-nginx created
```

List all services in the _ingress-nginx_ namespace:

```shell
> kubectl get services -n ingress-nginx
NAME            TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx   NodePort   10.98.52.92   <none>        80:32112/TCP,443:30923/TCP   74s
```

Here we see that the port 80 is dynamically mapped to the 32112 (you will most probably get a different mapping). 

Listing all exposed services using Minikube confirms that the Ingress Controller is available:

```shell 
> minikube service list
|---------------|----------------------|--------------------------------|
|   NAMESPACE   |         NAME         |              URL               |
|---------------|----------------------|--------------------------------|
| default       | kubernetes           | No node port                   |
| ingress-nginx | ingress-nginx        | http://192.168.39.12:32112     |
|               |                      | http://192.168.39.12:30923     |
| kube-system   | kube-dns             | No node port                   |
| kube-system   | kubernetes-dashboard | No node port                   |
|---------------|----------------------|--------------------------------|
```

_192.168.39.12_ is the IP address allocated to our Minikube VM. 
You can also get it with the `minikube ip` command.  

Open the URL http://192.168.39.12:32112 in a web browser, you will see a NGINX 404 page:

![Ingress NGinx 404](https://blog.octoperf.com/img/blog/kubernetes-ingress-nginx-frontend/ingress-nginx-404.png "Ingress NGinx 404")

Our NGINX controller is responding!

### Expose NodePort 80

By default, Kubernetes is configured to expose NodePort services on the port range 30000 - 32767.
But this port range can be configured, allowing us to use the port 80 for our Ingress Controller.

> Be warned though that [this is discouraged](https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#over-a-nodeport-service).

Start by deleting our existing minikube VM with the command:
 
```shell
> minikube delete
🔥Deleting "minikube" from kvm2 ...
The "minikube" cluster has been deleted.
```

Then restart it with the option `apiserver.service-node-port-range=1-30000`:

```shell
> minikube start --vm-driver=kvm2 --extra-config=apiserver.service-node-port-range=1-30000
😄  minikube v1.2.0 on linux (amd64)
💡  Tip: Use 'minikube start -p <name>' to create a new cluster, or 'minikube delete' to delete this one.
🏃  Re-using the currently running kvm2 VM for "minikube" ...
⌛  Waiting for SSH access ...
🐳  Configuring environment for Kubernetes v1.15.0 on Docker 18.09.6
    ▪ apiserver.service-node-port-range=1-30000
🔄  Relaunching Kubernetes v1.15.0 using kubeadm ... 
⌛  Verifying: apiserver proxy etcd scheduler controller dns
🏄  Done! kubectl is now configured to use "minikube"
```

Start the Ingress Controller and wait for it to start (`kubectl get pods -n ingress-nginx --watch`):

```shell
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
```

**Now that we can allocate the port 80, we also need to configure the NodePort service and expose the Ingress controller on this port.**
First download the configuration file:

```shell
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/service-nodeport.yaml
```

And update it (`service-nodeport.yaml`), to add `nodePort: 80` for the http entry and `nodePort: 443` for the https one:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
      nodePort: 80
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
      nodePort: 443
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
```

Apply the updated configuration:

```shell
> kubectl apply -f service-nodeport.yaml 
service/ingress-nginx created
```

Our Ingress Controller is now available on port 80 for HTTP and 443 for HTTPS:

```shell
> minikube service list
|---------------|----------------------|--------------------------------|
|   NAMESPACE   |         NAME         |              URL               |
|---------------|----------------------|--------------------------------|
| default       | kubernetes           | No node port                   |
| ingress-nginx | ingress-nginx        | http://192.168.39.146:80       |
|               |                      | http://192.168.39.146:443      |
| kube-system   | kube-dns             | No node port                   |
| kube-system   | kubernetes-dashboard | No node port                   |
|---------------|----------------------|--------------------------------|
```

You may think "Why don't we expose our frontend application directly using NodePort Service?".
That could also be done and it's probably the simplest solution ... for testing purpose.
But it's not manageable in a production environment with several frontend applications and backends running.  

### Troubleshooting

Remember that you can list Pods with the command `kubectl get pods -n ingress-nginx --watch`.

- The `-n` parameter specifies the namespace, here _ingress-nginx_ which is used by the NGinx Ingress controller,
- The `--watch` parameter refreshes the Pods list every time a modification occurs,
- Use the parameter `-A` to list resources for all namespaces. 

In case your Pod is stuck with the status __CreatingContainer__, you can display a list of events that may let you know what is going on with the `describe` command: 

```shell
> kubectl describe pod nginx-ingress-controller-7995bd9c47-cnjl8 -n ingress-nginx
Name:           nginx-ingress-controller-7995bd9c47-cnjl8
Namespace:      ingress-nginx
Priority:       0
Node:           minikube/192.168.122.244
Start Time:     Sun, 11 Aug 2019 17:05:05 +0200
Labels:         app.kubernetes.io/name=ingress-nginx
                app.kubernetes.io/part-of=ingress-nginx
                pod-template-hash=7995bd9c47
Annotations:    prometheus.io/port: 10254
                prometheus.io/scrape: true
Status:         Pending
IP:             
Controlled By:  ReplicaSet/nginx-ingress-controller-7995bd9c47
[...]
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  12m   default-scheduler  Successfully assigned ingress-nginx/nginx-ingress-controller-7995bd9c47-cnjl8 to minikube
  Normal  Pulling    12m   kubelet, minikube  Pulling image "quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.0"
```

Once a Pod is started, you can display the container logs with the command:

```shell
> kubectl logs nginx-ingress-controller-7995bd9c47-cnjl8 -n ingress-nginx
-------------------------------------------------------------------------------
NGINX Ingress controller
  Release:    0.25.0
  Build:      git-1387f7b7e
  Repository: https://github.com/kubernetes/ingress-nginx
-------------------------------------------------------------------------------

W0811 15:18:51.620335       6 flags.go:221] SSL certificate chain completion is disabled (--enable-ssl-chain-completion=false)
nginx version: openresty/1.15.8.1
W0811 15:18:51.624717       6 client_config.go:541] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I0811 15:18:51.624923       6 main.go:183] Creating API client for https://10.96.0.1:443
I0811 15:18:51.632485       6 main.go:227] Running in Kubernetes cluster version v1.15 (v1.15.0) - git (clean) commit e8462b5b5dc2584fdcd18e6bcfe9f1e4d970a529 - platform linux/amd64
I0811 15:18:51.719690       6 main.go:102] Created fake certificate with PemFileName: /etc/ingress-controller/ssl/default-fake-certificate.pem
E0811 15:18:51.720426       6 main.go:131] v1.15.0
```

## Deploy an Angular 8 Frontend

###  Angular8 Frontend Docker Image

Check out this blog post to know more about the creation of a Docker image for an Angular app: [Packaging Angular apps as docker images](https://blog.octoperf.com/angular-workspaces-multi-application-projects/#packaging-angular-apps-as-docker-images).  

**We will need to configure an URL rewrite rule in our Ingress object.**
Check this chapter to know more about this: [How to serve multiple Angular app with HAProxy](https://blog.octoperf.com/angular-workspaces-multi-application-projects/#how-to-serve-multiple-angular-applications-with-haproxy).

### How to Create a Deployment?

Start by creating the following configuration file, named `frontend-deployment.yaml`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
spec:
  selector:
    matchLabels:
      app: frontend
  minReadySeconds: 5
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - image: octoperf/administration-ui:1.2.1
          name: frontend
          ports:
            - containerPort: 80
```

**A Kubernetes Deployment is responsible for starting Pods on available cluster Nodes.**
Since our Pods contain a Docker container, the file above specifies the image, name and port mapping to use.

Apply the created configuration with the command:

```shell
> kubectl apply -f frontend-deployment.yaml
deployment.apps/frontend-deployment created
```

Finally, check that the deployment has started one pod:

```shell
> kubectl get deployments
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
frontend-deployment   1/1     1            0           7s
```

Here we can see the `READY 1/1` column, it's the number of Pods ready and the total that must be started.

### How to Expose a Deployment with a Service?

Like for the Deployment, create a configuration file and apply it.
The configuration file is named `frontend-service.yaml`:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

Apply it:

```shell
> kubectl apply -f frontend-service.yaml 
service/frontend-service created
```

There is no type and no _nodePort_ defined in this service. 
We only use it to **regroup a logical set of Pods and make them accessible from inside the K8s cluster**. 

```shell
> kubectl get services
NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
frontend-service   ClusterIP   10.103.204.81   <none>        80/TCP    9s
kubernetes         ClusterIP   10.96.0.1       <none>        443/TCP   44m
```

Here we can see that the `PORT(S)` column display only `80/TCP` for the _frontend-service_.
Not the usual `80:30001/TCP` notation for an exposed port. 

### How to Create an Ingress Object to Publicly Expose an App? 

If not already done, you first need to [have installed an Ingress Controller](#install-and-configure-nginx-ingress).

Then create the following configuration file named `frontend-ingress.yaml`: 

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/proxy-read-timeout: "12h"
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
  name: frontend-ingress
  namespace: default
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: frontend-service
              servicePort: 80
            path: /administration(/|$)(.*)
```

Ingress resources configuration is done using annotations:

- `ingress.class` should always be `"nginx"` unless you have [multiple Ingress Controllers running](https://kubernetes.github.io/ingress-nginx/user-guide/multiple-ingress/),  
- `rewrite-target` is used to skip the `/administration` part or the URL when forwarding requests to the frontend Docker container ([URL Rewrite documentation](https://kubernetes.github.io/ingress-nginx/examples/rewrite/)),
- `proxy-read-timeout` sets a timeout for [SSE connections](https://stackoverflow.com/questions/21630509/server-sent-events-connection-timeout-on-node-js-via-nginx),
- `ssl-redirect` is used to deactivate Https redirection since we are not specifying a host: The default-server is called without any host, this server is configured with a self-signed certificate (Would display a big security warning in your browser).

Apply the configuration:

```shell
> kubectl apply -f frontend-ingress.yaml 
ingress.extensions/frontend-ingress created
```

And check that it is OK:

```shell
> kubectl get ingresses
NAME               HOSTS   ADDRESS   PORTS   AGE
frontend-ingress   *                 80      29s
```

## Testing the Installation

If you [configured your Ingress Controller to be exposed on port 80](#expose-nodeport-80), the `minikube service list` will display a similar result:

```shell
> minikube service list
|---------------|----------------------|--------------------------------|
|   NAMESPACE   |         NAME         |              URL               |
|---------------|----------------------|--------------------------------|
| default       | frontend-service     | No node port                   |
| default       | kubernetes           | No node port                   |
| ingress-nginx | ingress-nginx        | http://192.168.39.162:80       |
|               |                      | http://192.168.39.162:443      |
| kube-system   | kube-dns             | No node port                   |
| kube-system   | kubernetes-dashboard | No node port                   |
|---------------|----------------------|--------------------------------|
```

You can display the port mapping with the following command otherwise:

```shell
> kubectl get services -n ingress-nginx
NAME            TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                 AGE
ingress-nginx   NodePort   10.101.112.201   <none>        80:80/TCP,443:443/TCP   32m
```

Opening this URL http://192.168.39.162:80/administration (the IP address is probably different for you) in your Web browser display deployed application.  

## Conclusion

You may also be interested in my other blog post about K8s: [How to deploy InfluxDB/Telegraf/Grafana on Kubernetes?](https://blog.octoperf.com/how-to-deploy-influxdb--telegraf--grafana-on-k8s/) or by other [blog posts related to DevOps](https://blog.octoperf.com/categories/devops/).
