Getting started with Kubernetes

Alright, we saw how to get started with development using docker here. Now, let’s setup Kubernetes (K8s) and move our existing app to K8s.

Again, the intent here is an introduction of K8s. Hence, I will not be going into the details but just touching the surface and moving our existing App to K8s. So, let’s get started.

Here is what we will be covering —

  1. Setting up K8s using Docker Desktop or manually using hyperkit and minikube
  2. Moving the mysql database to K8s
  3. Moving the redis instance to K8s
  4. Moving the App to K8s
  5. Getting it all together by Kubernetes magic

Source code in Github: https://github.com/sameerbhatt/url-shortener-app

Setting up K8s -

Install Docker Desktop to get most of the things automatically. You can Enable Kubernetes from Docker Desktop → Settings → Kubernetes

If not using Docker Desktop, install them manually — hyperkit and minikube

brew update
brew install hyperkit
brew install minikube
// Creating a minikube cluster
minikube start --vm-driver=hyperkit

Now, you might not be able to use your local images from Docker Desktop. See details here on how to fix -https://github.com/kubernetes/minikube/blob/0c616a6b42b28a1aab8397f5a9061f8ebbd9f3d9/README.md#reusing-the-docker-daemon

For this example, we will continue to use Docker Desktop. For this, you have to set the correct context now —

// Check the context
kubectl config current-context
// Set context as docker desktop
kubectl config use-context docker-desktop
// check version to see you get both Client and Server Versions
kubectl version

Let’s move to the next step. We will try to minimize the changes to our existing apps so that there is no change when used directly with docker or with Kubernetes.

Kubenetes Basics
Before going forward, I would highly encourage you to learn about Kubernetes and its components.

Moving Mysql database to K8s

Create a directory kube-config in the url-shortener-app and create a mysql.yaml file as follows -

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-url-shortener
spec:
selector:
matchLabels:
app: mysql-url-shortener
template:
metadata:
labels:
app: mysql-url-shortener
spec:
containers:
- image: mysql:8
name: mysql-url-shortener
imagePullPolicy: Never
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysecrets
key: mysql-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-volume
mountPath: /var/lib/mysql
volumes:
- name: mysql-volume
hostPath:
# directory location on host
path: /Users/sameer/personal/mysql-data
---
apiVersion: v1
kind: Service
metadata:
name: mysql-url-shortener
spec:
selector:
app: mysql-url-shortener
ports:
- port: 3306
targetPort: 3306

Let’s analyze it now -

  1. apiVersion, kind, metadata and other common fields, read here— https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
  2. Image related —
    image: mysql:8 — image name and tag
    imagePullPolicy: Never — this is important since our image is available locally, we don’t want to fetch it from the global docker registry for now. There are various options to use here based on your requirements
  3. Volume — Similar to how we used a local host directory for storing all mysql data for persistence in Docker, we will need the same path so that our existing data remains. hostPath is used here as I wanted to continue from our previous example. You can use persistentVolumeClaim as well. https://kubernetes.io/docs/concepts/storage/volumes/
  4. Service — you now need to expose the mysql database as a network service. https://kubernetes.io/docs/concepts/services-networking/service/
    The specification shown above creates a new Service object named “mysql-url-shortener”, which targets port 3306 on any Pod with the app=mysql-url-shortener label.

Note: There is no service type here, which mean it will default to ClusterIP. ClusterIP: Exposes the Service on a cluster-internal IP. Choosing this value makes the Service only reachable from within the cluster. For our Node App, we will be using a different type. Stay tuned to read more!

Setting up Secrets If you are wondering, we missed to discuss about secretKeyRef, don’t worry, we will go over it now. Here is the snippet that I’m talking about in mysql.yaml file-

name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysecrets
key: mysql-password

Basically, Kubernetes Secrets let you store and manage sensitive information, such as passwords, OAuth tokens, and ssh keys.

Refer details here — https://kubernetes.io/docs/concepts/configuration/secret/ https://kubernetes.io/docs/tasks/configmap-secret/managing-secret-using-config-file/

So, we will get the base64 value of our mysql password and use it as the value of the mysql-password field in the secrets file (see below). And then refer the secret in our mysql yaml file (as shown above).

echo -n 'root123' | base64
// outputs cm9vdDEyMw==

Here is the content of the secrets.yaml file —

apiVersion: v1
kind: Secret
metadata:
name: mysecrets
type: Opaque
data:
mysql-password: cm9vdDEyMw==

Moving Redis to K8s

This is mostly similar to the mysql setup above. So, in the directory kube-config in the url-shortener-app, create a redis.yaml file as follows -

apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-url-shortener
spec:
selector:
matchLabels:
app: redis-url-shortener
template:
metadata:
labels:
app: redis-url-shortener
spec:
containers:
- name: redis-url-shortener
image: redis:alpine
imagePullPolicy: Never
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-url-shortener
spec:
selector:
app: redis-url-shortener
ports:
- port: 6379
targetPort: 6379

If you notice, most of the things are same as mysql yaml file, except the name, image and ports. So, let’s not spend more time on it.

Moving the App to K8s

This is also similar to the above two setups, except a few fields that we will cover. So, in the directory kube-config in the url-shortener-app, create a app.yaml file as follows -

apiVersion: apps/v1
kind: Deployment
metadata:
name: url-shortener-deployment
spec:
replicas: 1
selector:
matchLabels:
app: url-shortener-app
template:
metadata:
labels:
app: url-shortener-app
spec:
containers:
- name: url-shortener-app
image: url-shortener-app:1.0
imagePullPolicy: Never
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: url-shortener-app
spec:
selector:
app: url-shortener-app
ports:
- port: 3000
targetPort: 3000
type: LoadBalancer

Fields to notice here -

  1. replicas: 1 — here, you can specify the number of replicas you want for your app depending on the expected traffic. Default is 1.
  2. image: url-shortener-app:1.0 — This is our App image that we built using docker.
  3. Service type: LoadBalancer — This is an important field. Since, we want to expose our service externally, we have changed to type LoadBalancer. From the documentation -

LoadBalancer: Exposes the Service externally using a cloud provider’s load balancer. NodePort and ClusterIP Services, to which the external load balancer routes, are automatically created.

Getting everything together

It’s time for some Kubernetes magic now. Make sure your kube-config directory have the following files — app.yaml, mysql.yaml, redis.yaml and secrets.yaml

$ kubectl apply -f kube-config
deployment.apps/url-shortener-deployment created
service/url-shortener-app created
deployment.apps/mysql-url-shortener created
service/mysql-url-shortener created
deployment.apps/redis-url-shortener created
service/redis-url-shortener created
secret/mysecrets created

Once, this is done, you can see the status as follows -

$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/mysql-url-shortener-bcb64d6fd-hcgfd 1/1 Running 0 65s
pod/redis-url-shortener-9768b8985-9qzbv 1/1 Running 0 65s
pod/url-shortener-deployment-77bd88587d-525b2 1/1 Running 0 65s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d21h
service/mysql-url-shortener ClusterIP 10.107.253.197 <none> 3306/TCP 65s
service/redis-url-shortener ClusterIP 10.109.49.50 <none> 6379/TCP 65s
service/url-shortener-app LoadBalancer 10.105.241.67 localhost 3000:30059/TCP 65s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mysql-url-shortener 1/1 1 1 65s
deployment.apps/redis-url-shortener 1/1 1 1 65s
deployment.apps/url-shortener-deployment 1/1 1 1 65s
NAME DESIRED CURRENT READY AGE
replicaset.apps/mysql-url-shortener-bcb64d6fd 1 1 1 65s
replicaset.apps/redis-url-shortener-9768b8985 1 1 1 65s
replicaset.apps/url-shortener-deployment-77bd88587d 1 1 1 65s

The above is mostly self-explanatory. There are pods, services, deployments and replicasets created.
Test it out now by opening http://localhost:3000/ in the browser.

Important: At times, you might notice the following status for the App Pod (notice the RESTARTS column) -

NAME                                            READY   STATUS    RESTARTS   AGE
pod/url-shortener-deployment-77bd88587d-r7xbp 1/1 Running 2 35s

What this means is that there was some error and the Pod crashed and then restarted twice before being ready. Give it some time to think over why it’s happening and then read further 🙂
The error lies in our code in db.js
We are trying to connect to the database only once and failing on error. What if, the mysql Pod isn’t ready by then. Our code will continue to crash till then. Kubernetes comes to the rescue by starting the Pod again and in this case, after 2 re-tries, it connects as the mysql Pod is up. That said, we should fix this in our code to keep trying for sometime to connect to the mysql database instead of trying once and failing.

To bring the Cluster down -

$ kubectl delete -f kube-config
deployment.apps "url-shortener-deployment" deleted
service "url-shortener-app" deleted
deployment.apps "mysql-url-shortener" deleted
service "mysql-url-shortener" deleted
deployment.apps "redis-url-shortener" deleted
service "redis-url-shortener" deleted
secret "mysecrets" deleted

Also, you might have noticed that there is no changes required in our original code-base. Everything just works. We were able to refer to the mysql and redis instances by the same names because we used same names in the mysql.yaml and redis.yaml files.

Here are some useful Kubernetes commands —

// minikube status
minikube status
// status
kubectl get all/ nodes/ pod/ service/ replicaset
// Edit a deployment, you don't need to apply/ delete/ create anything. It's managed automatically by K8s
kubectl edit deployment NAME
// show logs for a Pod, add -f to stream the logs
kubectl logs POD_NAME
// delete deployment
kubectl delete deployment NAME
// open terminal/shell of the Pod. it = iterative terminal
kubectl exec -it POD_NAME -- bash
// more details for a pod including IP address
kubectl get pod -o wide
// scale up/down a deployment, change value of replicas
kubectl scale deployment url-shortener-deployment --replicas=1;
// details of all services
kubectl get svc
// details for a service
kubectl describe service NAME
// details of the stored secrets
kubectl get secret mysecrets -o yaml

And finally — https://kubernetes.io/docs/reference/kubectl/cheatsheet/

That’s all for now. I hope, this intro helped you get started with Kubernetes :-)

I write about Technology, Leadership & Life in general. Views are personal.