Introduction to K8s using Minikube

18 minute read

What is Kubernetes?

Kubernetes helps us manage containerized applications that contain hundreds or thousands of containers in the different deployment environments, for example, physical machines, virtual machines, or cloud environments.

Kubernetes is a popular container orchestration platform that allows developers to deploy, manage, and scale containerized applications.

What is MiniKube?

Minikube is an open-source tool that helps developers (us) to run a single-node Kubernetes cluster on our local machine.

minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured

Minikube is a tool that allows you to run a Kubernetes cluster locally on your machine for development and testing purposes.

The minikube status command is used to check the status of the Minikube cluster.

The output above shows the current status of the minikube cluster. Here’s what each line means:

type: Control Plane : This indicates that the minikube cluster is running as a control plane, which means it has the ability to schedule and manage containers.

host: Running: This indicates that the virtual machine hosting the minikube cluster is currently running.

kubelet: Running: This indicates that the Kubernetes agent, kubelet, is running on the virtual machine and is responsible for managing containers on the node.

apiserver: Running: This indicates that the Kubernetes API server, which is responsible for managing the state of the Kubernetes cluster, is running.

kubeconfig: Configured: This indicates that the kubeconfig file, which contains the necessary credentials to access the Kubernetes API server, has been properly configured.

Example of K8 Setup for a Basic Web App using Flask

Create Docker Image from our Simple Flask Web App

Heres the basic file structure of the app below:

.
├── Dockerfile
├── app
│   ├── app.py
│   └── templates
│       ├── index.html
├── docker-compose.yaml
└── requirements.txt

docker-compose.yaml

We’d use this compose file to spin up a n number of container at thier respectable ports using the same image.

version: '3'
services:
  container1:
    image: burger
    ports:
      - "8080:6000"
  container2:
    image: burger
    ports:
      - "8081:6000"
  container3:
    image: burger
    ports:
      - "8082:6000"
  container4:
    image: burger
    ports:
      - "8083:6000"
  container5:
    image: burger
    ports:
      - "8084:6000"

To start up our containers, we’d use the docker-compose up command.

app.py File:

from flask import Flask, render_template
import json, os, signal

app = Flask(__name__)

@app.route("/")
def index():
    return render_template('index.html')


@app.route('/exit', methods=['GET'])
def stopServer():
    os.kill(os.getpid(), signal.SIGINT)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=6000)

Our Basic Flask App has two endpoints. With the first endpoint / rendering an index.html file (below) and another endpoint at /exit, which will STOP THE SERVER (which will come in handy later)

requirements.txt

flask==1.0.2

index.html

<!DOCTYPE html>
<html>
  <body>
    <iframe width="560" height="315" src="https://www.youtube.com/embed/9cPxh2DikIA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
  </body>
</html>

This html file is very simple and displays a youtube video which can be changed if you update the src with whatever Youtube link. I asked ChatGPT to creater a simple html file for me that display a youtube video.

Docker File

FROM python:3.6

COPY . /app

WORKDIR /app

RUN pip install -r requirements.txt

EXPOSE 8080

ENTRYPOINT ["python"]

CMD ["app/app.py"]

What does EXPOSE stand for?

In a Dockerfile, EXPOSE instruction is used to inform Docker which port the container should listen on for incoming network connections.

In the given Dockerfile, EXPOSE 8080 is used to inform Docker that the container will listen for incoming connections on port 8080. This doesn’t actually publish the port, but it is a way of documenting which ports the container is expected to use.

To actually publish the port so that external clients can access the container, you will need to use the -p option when running the docker run command to bind the container’s port to a port on the host machine. For example, you could run the container with the command docker run -p 8080:8080 image_name to publish the container’s port 8080 to the host’s port 8080.

Note that the EXPOSE instruction does not actually start the container listening on the specified port, it simply documents which ports are expected to be used. The actual process of starting the container and listening on the specified port is handled by the application running inside the container.

Lets build the Image

In the terminal and in our current app directory, the name name-of-image is the name we decided for our Image, using the -t tag.

docker build -t "name-of-image" .

Start Minikube Kuberentes Cluster

  • Since were running it in Docker

Were creating a virtual enviroment (containizer) enviroment for this cluster, the cluster that we’re simulating on our local machine to test K8. This cluster (in our example) is created using Docker.

We use the kubectl command to send information (on our local machine) to the K8 Cluster (Master Node and Control Plane)! Kubectl = Kube Control which is our communication device with the Master Node.

minikube start --driver=docker

With our Cluster up and running, we can send instructions to create deployment to the cluster. How do we accomplish this using kubectl command.

We want to use the kubectl to create **new deployment object** (remember we work with these objects, which are then picked up by the K8 Cluster) We can create objects with the kubectl create command

Were going to create a deployment object, which is more common object that you will create.

This Object is automatically send to the K8 Cluster, we can give it a name . Then we have to add --image= option, which will will specify which image we should be used for the container.

Note: We can’t use our local image that we created above. Instead we have to upload our image to our Docker Image Repository online.

Lets push our image that we created above to DockerHub

Go to Dockerhub

need to update here

Lets retag our image for our app that we Dockerized above as as our Docker Hub Account name

kubectl create deployment first-app --image=devinpowers/burger-repo

Output:

deployment.apps/first-app created

How to delete a Deployment

Use the command kubectl delete deployment name_of_deployment

kubectl delete deployment first-app 

deployment.apps "first-app" deleted

Now if we check deployments in the terminal:

kubectl get deployment

Output:

NAME        READY   UP-TO-DATE   AVAILABLE   AGE
first-app   1/1     1            1           2m22s

We have 1 ready Deployment. Then if we run kubectl get pods:

(base) devinpowers@Devins-MacBook-Pro basic app % kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
first-app-7bbc4c4c49-2hvn5   1/1     Running   0          3m40s

We will also have 1/1 Ready. Our application is up and running.

We can’t reach it yet (comming)

We can see the Web Broswer UI using the command: minikube dashboard, which will take us to

"insert web ua here"

Video 188

Video 190: exposing Pod

Creating a Service so we can vist our Flask App that we made above!

  • Port 6000 because thats what we exposed in the Dockerfile!
kubectl expose deployment first-app --type=LoadBalancer --port=6000

Service created:

service/first-app exposed
kubectl get services

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
first-app    LoadBalancer   10.110.114.205   <pending>     8080:30386/TCP   82s
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   69d
minikube service first-app

Output from the terminal:


|-----------|-----------|-------------|---------------------------|
| NAMESPACE |   NAME    | TARGET PORT |            URL            |
|-----------|-----------|-------------|---------------------------|
| default   | first-app |        8080 | http://192.168.49.2:30386 |
|-----------|-----------|-------------|---------------------------|

 Starting tunnel for service first-app.
|-----------|-----------|-------------|------------------------|
| NAMESPACE |   NAME    | TARGET PORT |          URL           |
|-----------|-----------|-------------|------------------------|
| default   | first-app |             | http://127.0.0.1:52110 |
|-----------|-----------|-------------|------------------------|

This will open a link to our Web App!

Video 191: Restarting Containers

One of the Big Concepts and uses of Kubernetes is *Managing. If for example we went to the ./exit endpoint in our web app, like we did earlier above, our pod stops! As we can see when we type kubectl get pods into the terminal as seen below:

kubectl get pods

NAME                         READY   STATUS             RESTARTS      AGE
first-app-64d94d9ffc-9jt5x   0/1     CrashLoopBackOff   4 (38s ago)   6h57m

If we wait a little bit, we will see that the Ready satus will update to 1/1 and that kubernetes restarted our Pod for us! We didn’t have to do it manually.

Video 192: Scaling in Action

kubectl scale deployment/first-app --replicas=3
kubectl get pods

NAME                         READY   STATUS    RESTARTS        AGE
first-app-64d94d9ffc-2n6jm   1/1     Running   2 (2m55s ago)   4m52s
first-app-64d94d9ffc-p8nqd   1/1     Running   0               15s
first-app-64d94d9ffc-wkq5q   1/1     Running   0               15s

We can see that we now have 3 Pods

Video 193: Updating Deployments

We can update our deployments with a newer image. Lets see how we do this:

Lets first update the index.html to display a different youtube video. [x]

Then we need to rebuild our image.

docker build -t devinpowers/burger-repo .

Now we need to push our updated image to Dockerhub:

docker push devinpowers/burger-app

Output

Using default tag: latest
The push refers to repository [docker.io/devinpowers/burger-repo]
5b41cfe4b886: Pushing  11.96MB
5b41cfe4b886: Pushed 
a316ba36abf6: Pushed 
16697f967866: Layer already exists 
a8b3ae1d334a: Layer already exists 
7cdfdc39018d: Layer already exists 
28c914fab499: Layer already exists 
fef6f293382e: Layer already exists 
ffd50287b468: Layer already exists 
cba7a92f211b: Layer already exists 
fe09b9981fd2: Layer already exists 
dd5b48ca5196: Layer already exists 

latest: digest: sha256:89725ca61cb1dc3cbf8babd0901563bf6f60ac1933c521906f4880a02d388a8c size: 2843

Now we can update our Deployment using the set and then add image and the name of our deployment. Then we can grab the container-name

kubectl set image deployment/burger-app burger-repo=devinpowers/burger-repo

This will update the deployment.

Once we execute the set image and we go to the terminal and type kubectl get deployments:

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
burger-app   10/10   10           10          113m

Not that our web-app deployment WILL ONLY UPDATE if we update the image with a different tag, lets do this below. Note: Make sure we’re in our directory where we have the file we want to Dockerize.

docker build -t devinpowers/burger-repo:2 .

Now we can push this image with the tag at the end to our Dockerhub.

docker push devinpowers/burger-repo:2

With that finished we can apply the set image command again but this time include the tag.

kubectl set image deployment/burger-app burger-repo=devinpowers/burger-repo:2

Output We can see that it updated based on the output after setting the image again.

deployment.apps/burger-app image updated

This will let Kuberentes know that this is a new tag (version) and redownload the image and restart the containers based on it.

We can view the current updated status using the kubectl command below:

kubectl rollout status deployment/burger-app

Output:

deployment "burger-app" successfully rolled out

If we go back to our Browswer, we should see the updated web-app with a different video! Maybe wait a minute or two!

Video 194: Deployment Rollbacks & History

What is a Rollback in K8?

A roll-back in kubectl for Kubernetes (K8s) deployments refers to the process of reverting a deployment to a previous version or state. It involves undoing the changes made in the current deployment and restoring the previous deployment configuration.

In kubectl, a roll-back can be performed using the “kubectl rollout undo” command, which is used to revert to the previous deployment revision. When this command is executed, Kubernetes will create a new revision of the deployment that has the same configuration as the previous revision. This new revision will then be rolled out to the cluster, replacing the current revision.

The roll-back command can be used with various options to control the behavior of the roll-back process, such as the number of revisions to undo, the namespace, and the deployment name.

Roll-backs are important in Kubernetes as they allow you to quickly recover from issues or errors introduced by a deployment. By rolling back to a previous version of the deployment, you can revert to a known good state and avoid downtime or other issues caused by a problematic deployment.

Video 195: The Imperative vs The Declarative Approach

  • Using .yaml files for configuring everything instead of the imperative approach that we’ve been doing up to this point, where we have to type all the commands into our terminal.

Video 196: Creating a Deployment Configuration File

Lets create a deployment.yaml file in our directory for our Web Application as shown below:

 .
├── Dockerfile
├── app
│   ├── app.py
│   └── templates
│       ├── index.html
|___ deployment.yaml
└── requirements.txt

Lets configure our Deployment yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: second-app-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: second-app
    spec:
      containers:
        - name: second-app
          image: devinpowers/burger-repo:latest
      # - name:
      #   image: 

Here a link to the Reference docs for Building Yaml FIles for K8s

Here is a summary of the above yaml configuration for our App.

apiVersion: the Kubernetes API version of the Deployment object. In this case, it is using the apps/v1 API version.

kind: the type of Kubernetes Object being defined. In our case, it is a Deployment.

metadata: the metadata associated with the Deployment object. It includes the name of the Deployment, which is second-app-deployment. Can name it whatever we like!

spec: the desired state of the Deployment. It specifies the number of replicas (pods), which is set to 1. It also includes a selector that matches the labels of the Pods managed by the Deployment. In this case, it matches the app: second-app and tier: backend labels. The template field defines the Pod template that is used to create new Pods when necessary. It includes the labels that the Pods will have, which again match the selector labels, and a containers field that defines the container specification for the Pod. In this case, it defines a single container with the name second-node and the image academind/kub-first-app:2.

There are also two commented out sections under the containers field. These can be uncommented and modified to add additional containers to the Pod!

Note: In Kubernetes (often abbreviated as “K8s”), a “replica” refers to a set of identical instances of a pod. A pod is the smallest deployable unit in Kubernetes, and it can contain one or more containers.

Video 197: Adding Pods and Container Specs

RUn our

kubectl apply -f deployment.yaml

The kubectl command apply, simply applies a configuration file to the connected cluster, you identify the file with the -f (you can have multiple -f options, if you want to apply multiple files at once). Then you just add an =file_name.yaml. Or the path to the file.

If we do this, we get an error in the output:

error: error validating "deployment.yaml": error validating data: ValidationError(Deployment.spec): missing required field "selector" in io.k8s.api.apps.v1.DeploymentSpec; if you choose to ignore these errors, turn validation off with --validate=false

We did this on purpose to show selector are important concepts in the Kubernetes World.

Video 198: Working with Labels & Selectors

How do we fix this?

We go to the Specification of the Deployment, we also must include a selector key with a matchLabels and two nested below app and tier, as shown below:

  • We want to match with this deployment

Note that deployments are dynamic objects

A deployment continously watches all the pods which are out there and sees if any there are any pods should it control. AND it selects the to-be controled pods with a so-called selector! We will see selector in many resources Kubernetes works with! There are different types of selectors, for (kind) Deployment object, we can use two different types of selecting: matchLabels or matchExpressions.

This selector (below in our yaml file) is specifying that the Deployment should manage pods with the labels “app=second-app” and “tier=backend”. This means that the Deployment will only manage pods that have those exact labels.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: second-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: second-app
      tier: backend
  template:
    metadata: 
      labels:
        app: second-app
        tier: backend
    spec: 
      containers:
        - name: second-app
          image: devinpowers/burger-repo:latest
      # - name: ...
      #   image: ...

The first spec is for the overall Deployment. We add another spec, on the same level as metadata, indented below template and here we define how this pod should be configured. Specification of the individual pods which are created for this Deployment. We can have multiple containers defined using the - sign or dashes. In yaml formating the - stands for a list.

Here were using the image devinpowers/burger-repo:latest, which is from Dockerhub. Needs to an image on a registry. We used tag (version), which is the version.

How can we now apply this Deployment? How can we make the cluster aware of it, and have it create the Deployment and Pod, and launch that container?

Now if we try to apply again:

kubectl apply -f deployment.yaml

We get the output:

 deployment.apps/second-app-deployment created

now this deployment was created. And if we run kubectl get deployments we can see our deployment is up and running:

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
second-app-deployment   1/1     1            1           101s

And if we run kubectl get pods we can also see this up and running.

NAME                                     READY   STATUS    RESTARTS      AGE
second-app-deployment-86f4467b98-svvx9   1/1     Running   0             2m22s

All our configuration was done in the deployment.yaml file. Declariative Approach.

At the moment we can’t visit the App because the Service is missing. (more on this below):

Video 199: Creating a Service Declaratively

Lets create a Service resources for our App.

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector: 
    app: second-app
  ports:
    - protocol: 'TCP'
      port: 80
      targetPort: 8080
    # - protocol: 'TCP'
    #   port: 443
    #   targetPort: 443
  type: LoadBalancer

spec: This section specifies the desired state of the service. It includes:

selector: This specifies how Kubernetes should select the pods that the service should route traffic to. In this case, it uses the label “app=second-app” to select the pods.

ports: This specifies the ports that the service should listen on and route traffic to. In this case, there is only one port specified:

  • protocol: This specifies the network protocol that the port should use. In this case, it is set to “TCP”.

  • port: This specifies the port number that the service should listen on. In this case, it is set to port 80.

  • targetPort: This specifies the port number that the service should route traffic to on the pods. In this case, it is set to port 8080.

type: This specifies the type of the service. In this case, it is set to “LoadBalancer”. This means that the service will be exposed externally using a cloud provider’s load balancer, if available. This will provide a stable IP address for the service, and distribute traffic across the pods that the service is routing traffic to.

Services control Pods, if we remember that the Service Object Exposes Pods to the Cluster or Externally, we can’t reach a pod without Services!

  • Pods have an internal IP (address) by default -it changes when a Pod is Replaced
    • Finding Pods is hard if the IP changes all the time
  • Services group Pods with a shared IP
  • Services can allow exrternal access to Pods
    • The default (internal only) can be overwritten.

More on Services:

In Kubernetes, a Service is an abstraction layer that provides a stable network endpoint for accessing a set of pods. When a Service receives traffic from a client, it routes that traffic to one of the pods that it manages, based on a set of rules specified in the Service configuration.

In the YAML file you provided earlier, the “ports” field is used to specify the port number that the Service should listen on, as well as the target port number on the pods to which the Service should route the incoming traffic.

Here’s what the “port” and “targetPort” fields mean in the context of a Kubernetes Service:

  • Port: This is the port number that the Service should listen on for incoming traffic. When a client sends traffic to this port, the Service will route that traffic to one of the pods it manages, based on the Service’s routing rules.

  • TargetPort: This is the port number on the pods that the Service should route the incoming traffic to. When the Service receives traffic on its own port, it forwards that traffic to the target port on the selected pod.

For example, in the YAML file we have above:

ports:
  - protocol: 'TCP'
    port: 80
    targetPort: 8080

This specifies that the Service should listen on port 80, and route traffic to the pods on port 8080. So, when a client sends traffic to the Service’s port 80, the Service will forward that traffic to one of the pods it manages on port 8080.

By using separate port numbers for the Service and the target port on the pods, Kubernetes makes it easy to expose services to the external world, while still allowing the internal components of the application to communicate with each other using their own ports.

kubectl apply -f service.yaml

Output:

service/backend created

Now if we go kubectl get services we can see our service we created from the service.yaml file above:

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
backend      LoadBalancer   10.108.251.190   <pending>     80:32097/TCP     52s
kubernetes   ClusterIP      10.96.0.1        <none>        443/TCP        6s

Note that the Service named kubernetes is always running!

Now we can now expose the Service with Minikube Service with the name we gave it, which is named backend:

minikube service backend

Output:

minikube service backend
|-----------|---------|-------------|---------------------------|
| NAMESPACE |  NAME   | TARGET PORT |            URL            |
|-----------|---------|-------------|---------------------------|
| default   | backend |          80 | http://192.168.49.2:32097 |
|-----------|---------|-------------|---------------------------|
🏃  Starting tunnel for service backend.
|-----------|---------|-------------|------------------------|
| NAMESPACE |  NAME   | TARGET PORT |          URL           |
|-----------|---------|-------------|------------------------|
| default   | backend |             | http://127.0.0.1:52355 |
|-----------|---------|-------------|------------------------|
🎉  Opening service default/backend in default browser...
❗  Because you are using a Docker driver on darwin, the terminal needs to be open to run it.

What is TCP?

TCP (Transmission Control Protocol) is a protocol that governs the way data is transmitted over the Internet. It is one of the main protocols in the Internet protocol suite, which is a set of communication protocols that are used for transmitting data over networks.

TCP is a connection-oriented protocol, which means that it establishes a virtual connection between two devices before transmitting data. It ensures that data is delivered reliably, with error checking and correction mechanisms built in. TCP provides a mechanism for controlling the flow of data between two devices, so that one device does not overwhelm the other with too much data too quickly. It also provides a mechanism for retransmitting lost packets, so that data is not lost during transmission.

TCP is used by many applications on the Internet, including web browsers, email clients, and file transfer protocols. When a client sends a request to a server, TCP is used to establish a connection between the two devices. Once the connection is established, data can be transmitted back and forth between the client and server.

Overall, TCP is a critical protocol for ensuring reliable data transmission over the Internet, and it is an essential component of the modern digital infrastructure that powers the Internet.

Extra Showing the high overview Architecture of K8 using Minikube vs using a Cloud Service like Azure

Extra Notes on K8 Architecture

Part 1

https://aws.plainenglish.io/what-is-kubernetes-k8s-and-main-kubernetes-components-1-50919e5fd066

Part 2:

https://aws.plainenglish.io/kubernetes-architecture-2-38f91beb298c

Part 3:

https://aws.plainenglish.io/kubernetes-local-setup-using-minikube-and-kubectl-3-a46b7effe693