Kubernetes Basics

Learning essential components of Kubernetes

Manato Kuroda
14 min readJan 29, 2023
Photo by Chirayu Sharma on Unsplash

In this article, we will have a look at the main components of Kubernetes and their usage through a simple example.

Scope of this article

This article does not attempt to give advanced instructions on how to use Kubernetes. Instead, it is designed to provide basic concepts and instructions for developers who want to play around with Kubernetes in their local environment and to get an overall picture of Kubernetes through practice.

Pre-requisites

In order to execute Kubernetes on your local machine, the Kubernetes option needs to be enabled in the Docker Desktop. (If you haven’t installed the Docker Desktop, go to the download page and install it.)

Go to the Docker Desktop > Preferences > Kubernetes and enable the option:

Docker Desktop

Kubernetes Architecture

The key idea behind Kubernetes is to keep your application in the desired state by continuously comparing the actual state with the desired state.

The key idea of Kubernetes

In a nutshell, what we need to do is to define the desired state in a YAML file and pass it to Kubernetes, then Kubernetes will manage and monitor your application. To achieve this, Kubernetes consists of a set of components below:

https://kubernetes.io/docs/concepts/overview/components/

Control Plane

Control Plane is responsible for managing your application in the desired state. The kube-apiserver exposes the Kubernetes API and allows you to send your desired state (YAML file) to the Control Plane. Once you send the desired state through the kube-apiserver , Kubernetes stores the state data into etcd.

Control Plane

Node provides a Kubernetes runtime environment and allows you to run an application, which means that Node has the actual state of your application.

Node

The kubelet on each Node stores the actual state into the etcd through the API.

Node

Control Plane has the desired state and the actual state inside the etcd store and checks if they are different, if so, changes the actual state to the desired state automatically.

Desired State and Actual State

If you want to learn more about Kubernetes components, take a look at the documentation, but for now, it’s okay to just grab the whole picture of it because Kubernetes has a lot of components, which looks kind of overwhelming for beginners.

Cluster

A Kubernetes Cluster includes the control plane and a set of nodes that run your applications. A Cluster normally runs multiple nodes for high availability and fault tolerance.

Kubernetes cluster

When you install the Docker Desktop and enable the Kubernetes option, the default cluster will be installed on your local machine, it’s called docker-desktop.

Let’s list the clusters in the terminal. Run the kubectl config get-contexts and should look like this:

$ kubectl config get-contexts

(git)-[main]
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* docker-desktop docker-desktop docker-desktop

Multiple clusters can be installed on your machine. When you want to check the current cluster that you connect to, run the kubectl config current-context , then docker-desktop should show up.

$ kubectl config current-context


docker-desktop

In this article, we will implement Kubernetes components in this cluster. However, if you want to add another cluster on your local machine, install the Kind. Kind is a tool for running Kubernetes Clusters for your local development or CI.

$ brew install kind 

Once installed, create a cluster:

$ kind create cluster

Let’s list the clusters again, then you will see the kind-kind cluster:

$ kubectl config get-contexts

CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* docker-desktop docker-desktop docker-desktop
kind-kind kind-kind kind-kind

If you want to switch the current cluster to the kind-kind, run the command:

$ kubectl config use-context kind-kind

As you can see the current cluster has changed:

$ kubectl config current-context

kind-kind

To delete a cluster, run the command:

kind delete cluster

Pods

Pods are the smallest units of the component to deploy in Kubernetes. A pod can contain one or multiple containers and share the same storage and network resources. Basically, a pod often contains one container, and in actual development, it is not recommended to include multiple containers or applications in a single pod. Instead, it is recommended to deploy multiple pods with a single application for high availability.

Pods

Let us create a pod with an Nginx server.

Create a YAML file called test-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx

And apply the pod by running the command:

$ kubectl apply -f test-pod.yaml

pod/nginx created

In order to check the pod running in your cluster, run the kubectl get pods :

$ kubectl get pods

NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 47s

To get more information about a pod, run this command. (If you haven’t installed the jq, install it by brew install jq)

$ kubectl get pod nginx -o jsonpath="{.spec}" | jq

And the details should look like this. You can see that the pod pulls the Nginx image container.

{
"containers": [
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "nginx",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-hgccf",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"nodeName": "docker-desktop",
"preemptionPolicy": "PreemptLowerPriority",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 30,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
}

To delete a pod, run the command:

$ kubectl delete -f test-pod.yaml

pod "nginx" deleted

Kubernetes Objects

When you create an object in Kubernetes, you need to define the desired state and provide information to Kubernetes through the kubectl. In the last example of pods, we’ve defined the YAML file like so:

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx

The spec field is the desired state you must provide. In this case, the desired state means that a pod has to pull the Nginx container and run it.

apiVersion: v1
kind: Pod
metadata:
name: nginx

# desired state
spec:
containers:
- name: nginx
image: nginx // tells Kubernetes to pull the nginx container and run it.

Remember that the key idea of Kubernetes. All you need to do is to create a YAML file and pass it to Kubernetes, then Kubernetes will take care of it.

The key idea of Kubernetes

And the kubectl apply command, passing the YAML file will tell Kubernetes API to create a pod and run it.

Here are the required fields in a YAML file:

apiVersion: v1 # Kubernetes API version. ex.) apps/v1, v1
kind: Pod # What kind of object you want to create. ex.) Pod, Deployment, Service
metadata: # To indentify the object uniquely.
name: nginx
spec: # desired state.

Namespaces

Namespaces allow you to isolate groups of resources within a single cluster. Basically, Namespaces are intended for use in multiple environments or teams to organize objects in a cluster. For example, Namespaces can be used as production and development environments or front-end and back-end apps.

By default, Kubernetes starts with four initial namespaces:

$ kubectl get ns

NAME STATUS AGE
default Active 40d
kube-node-lease Active 40d
kube-public Active 40d
kube-system Active 40d

The default namespace is used when you create an object without specifying it.

If you create a pod by running the command like this:

$ kubectl apply -f test-pod.yaml

The pod will be created within the default namespace.

Run the kubectl get pods -A and you can see the NAMESPACE field:

$ kubectl get pods -A

NAMESPACE NAME READY STATUS RESTARTS AGE
default nginx 1/1 Running 0 55m

So, let’s create a custom namespace.

Create a YAML file called test-ns.yaml :

apiVersion: v1
kind: Namespace
metadata:
name: my-namespace

This will create a namespace called my-namespace.

Apply the file:

$ kubectl apply -f test.ns.yaml 

Check the namespaces again and the my-namespace has been created:

$ kubectl get ns

NAME STATUS AGE
default Active 40d
kube-node-lease Active 40d
kube-public Active 40d
kube-system Active 40d
kubernetes-dashboard Active 40d
my-namespace Active 25s

To create a pod in the my-namespace, add the option -n like so:

$ kubectl apply -f test-pod.yaml -n my-namespace 

List the pods and you can see that the pod was added in the my-namespace :

$ kubectl get pods -A  

NAMESPACE NAME READY STATUS RESTARTS AGE
my-namespace nginx 1/1 Running 0 20s

To delete the pod from the namespace, specify the namespace:

$ kubectl delete -f test-pod.yaml -n my-namespace

To delete the namespace, run this command:

$ kubectl delete -f test-ns.yaml

ReplicaSet

ReplicaSet is used to guarantee the availability of a specified number of pods. If your application needs multiple pods to be running in the node, ReplicaSet can ensure that a specified number of pod replicas are running at any given time.

ReplicaSet

Create a YAML file called test-replicaset.yaml and specify the replica number:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 2 # Specify replicas
selector:
matchLabels:
app: nginx # Define pod label
template: # Define template of pod to run
metadata:
labels:
app: nginx # Specify the above pod label
spec:
containers:
- name: nginx
image: nginx

Then apply the ReplicaSet to Kubernetes:

$ kubectl apply -f test-replicaset.yaml

replicaset.apps/nginx created

List the pods and you can see that two pods are running there:

$ kubectl get pods

NAME READY STATUS RESTARTS AGE
nginx-gpc97 1/1 Running 0 19s
nginx-pvfb5 1/1 Running 0 19s

To delete ReplicaSet, run the delete command:

$ kubectl delete -f test-replicaset.yaml

Since ReplicaSet only ensures the number of pod replicas and does not manage the deployment of pods, it is not recommended to use only ReplicaSet itself. Instead, a Deployment is used to manage pods and ReplicaSet.

Deployment

A Deployment provides an updates feature for Pods and ReplicaSets. You define a desired state in a Deployment so that the Deployment Controller can change the actual state to the desired state at a controlled rate.

Let’s create a YAML file called test-deployment.yaml.

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15

The fields are pretty much the same as the ones of ReplicaSet.

Then apply the Deployment to Kubernetes:

$ kubectl apply -f test-deployment.yaml

deployment.apps/nginx created

List the Deployments and you can see that three pods are running:

$ kubectl get deploy

NAME READY UP-TO-DATE AVAILABLE AGE
nginx 3/3 3 3 40s

List the ReplicaSet and Pods as well:

$ kubectl get rs

NAME DESIRED CURRENT READY AGE
nginx-67cddc5c44 3 3 3 107s
$ kubectl get pods

NAME READY STATUS RESTARTS AGE
nginx-67cddc5c44-9v28t 1/1 Running 0 2m10s
nginx-67cddc5c44-kvp66 1/1 Running 0 2m10s
nginx-67cddc5c44-rdmbv 1/1 Running 0 2m10s

Next, let’s change the Nginx container’s version and redeploy it.

Change the container version from v1.15 to v1.16 :

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
- image: nginx:1.15
+ image: nginx:1.16

To view the process of deployment, run the kubectl with watch mode on another terminal:

$ kubectl get pods -w

NAME READY STATUS RESTARTS AGE
nginx-67cddc5c44-9v28t 1/1 Running 0 7m58s
nginx-67cddc5c44-kvp66 1/1 Running 0 7m58s
nginx-67cddc5c44-rdmbv 1/1 Running 0 7m58s

And apply it:

$ kubectl apply -f test-deployment.yaml

Once applied, the Deployment automatically detects the change and re-deploys them with new container.

The another terminal should output the status like so:

$ kubectl get pods -w

NAME READY STATUS RESTARTS AGE
nginx-67cddc5c44-9v28t 1/1 Running 0 7m58s
nginx-67cddc5c44-kvp66 1/1 Running 0 7m58s
nginx-67cddc5c44-rdmbv 1/1 Running 0 7m58s
nginx-6c4b465475-8jl9x 0/1 Pending 0 0s
nginx-6c4b465475-8jl9x 0/1 Pending 0 0s
nginx-6c4b465475-8jl9x 0/1 ContainerCreating 0 0s
nginx-6c4b465475-8jl9x 1/1 Running 0 8s
nginx-67cddc5c44-9v28t 1/1 Terminating 0 11m
nginx-6c4b465475-q2zkb 0/1 Pending 0 0s
nginx-6c4b465475-q2zkb 0/1 Pending 0 0s
nginx-6c4b465475-q2zkb 0/1 ContainerCreating 0 0s
nginx-67cddc5c44-9v28t 0/1 Terminating 0 11m
nginx-67cddc5c44-9v28t 0/1 Terminating 0 11m
nginx-67cddc5c44-9v28t 0/1 Terminating 0 11m
nginx-6c4b465475-q2zkb 1/1 Running 0 2s
nginx-67cddc5c44-rdmbv 1/1 Terminating 0 11m
nginx-6c4b465475-5chxr 0/1 Pending 0 0s
nginx-6c4b465475-5chxr 0/1 Pending 0 0s
nginx-6c4b465475-5chxr 0/1 ContainerCreating 0 0s
nginx-67cddc5c44-rdmbv 0/1 Terminating 0 11m
nginx-67cddc5c44-rdmbv 0/1 Terminating 0 11m
nginx-67cddc5c44-rdmbv 0/1 Terminating 0 11m
nginx-6c4b465475-5chxr 1/1 Running 0 1s
nginx-67cddc5c44-kvp66 1/1 Terminating 0 11m
nginx-67cddc5c44-kvp66 0/1 Terminating 0 11m
nginx-67cddc5c44-kvp66 0/1 Terminating 0 11m
nginx-67cddc5c44-kvp66 0/1 Terminating 0 11m

And get the details of the pod and you can see that Nginx has changed to v1.16:

$ kubectl get pod nginx-6c4b465475-5chxr -o jsonpath="{.spec}" | jq

{
"containers": [
{
+ "image": "nginx:1.16",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-kjctg",
"readOnly": true
}
]
}
],

You can check the revisions of Deployment by running the command:

$ kubectl rollout history deployment/nginx

deployment.apps/nginx

REVISION CHANGE-CAUSE
1 <none>
2 <none>

The CHANGE-CAUSE is a message by annotating the Deployment with kubectl annotate or manually editing the YAML file. In this case, there are no CHANGE-CAUSE detected.

If you’ve decided to undo the current deployment and rollback to a previous revision, then run the rollout undo command:

$ kubectl rollout undo deployment/nginx --to-revision=1

deployment.apps/nginx rolled back

And the Nginx should go back to v1.15 like so:

$ kubectl get pod nginx-67cddc5c44-2vksj -o jsonpath="{.spec}" | jq

{
"containers": [
{
+ "image": "nginx:1.15",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-lwlxj",
"readOnly": true
}
]
}
],

Lastly, to delete the Deployment, run this command:

$ kubectl delete -f test-deployment.yaml

ConfigMap

A ConfigMap is used to store non-confidential data and referenced by Pods. By using ConfigMap, configuration data is separated from application code, so that your applications are more portable. The usecase of ConfigMap is to set environment variables, such as Database Host, User, and Port.

Let’s create a ConfigMap called test-configmap.yaml :

apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
API_URL: http://localhost:8081/api # Specify data

---
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
command: ["env"]
envFrom:
- configMapRef:
name: my-config

First, define a configuration data like so:

data:
API_URL: http://localhost:8081/api # Specify data

In a Pod, consume the data by configMapRef :

spec:
containers:
- name: nginx
image: nginx
command: ["env"]
+ envFrom:
+ - configMapRef:
+ name: my-config

Let’s check the environment variables by logging the pod:

$ kubectl logs nginx

HOSTNAME=nginx
+ API_URL=http://localhost:8081/api
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
NGINX_VERSION=1.23.3
...

Secret

A Secret stores sensitive data such as a token and a password. By using a Secret, you don’t have to contain confidential data in your application code.

Create a Secret called test-secret.yaml :

apiVersion: v1
kind: Secret
metadata:
name: mysecret
namespace: default
type: Opaque
data:
token: dG9rZW4= #token

---
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
command: ["env"]
envFrom:
- secretRef:
name: mysecret
restartPolicy: Never

Since Kubernetes Secrets are stored unencrypted in the API server’s store (etcd), in order to safely use Secrets, you need to encode data with base64. You can take other options and see information security for Secrets for more details.

In this example, we encode the string token with base64 like so:

$ echo -n 'token' | base64                                                                                                                                                                     (git)-[main]

dG9rZW4=

And then write the encoded data to:

apiVersion: v1
kind: Secret
metadata:
name: mysecret
namespace: default
type: Opaque
data:
+ token: dG9rZW4= #token

And reference the secret in a pod:

spec:
containers:
- name: nginx
image: nginx
command: ["env"]
+ envFrom:
+ - secretRef:
+ name: mysecret

Apply the Secret:

$ kubectl apply -f test-secret.yaml

Then you can see the environment variable in a Pod:

$ kubectl logs nginx

+ token=token
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
...

Services

Each Pod gets its own IP address, but each time it is created or deleted in a Deployment, the set of Pods has a different IP address. This could be a problem because the IP address to connect to each Pod must be tracked and consistent on your application. To solve this problem, Kubernetes Service provides DNS functionality to manage the IP address and allows you to keep track of each Pod in a Deployment. Pods targeted by a Service are labeled by a selector so that a Service can detect each pod.

Service

Let’s create a Service called test-service.yaml. Suppose we have a set of Pods where each listens on TCP port 80 and contains a label app=MyApp :

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp # for label port
ports:
- protocol: TCP
port: 80 # for service port
targetPort: 80 # for pod port

The port field is a port for a Service and the targetPort is a port for each pod. A Service can map any incoming request from port to a targetPort , but by default, the targetPort is set to the same value as the port field.

Service Port

Next, create a Pod called my-app-pod.yaml and label MyApp:

apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: MyApp
spec:
containers:
- name: nginx
image: nginx
restartPolicy: Never

Then apply them:

$ kubectl apply -f test-service.yaml
$ kubectl apply -f my-app-pod.yaml

Check the Pod with --show-labels option and you can see the label:

$ kubectl get pod --show-labels   
(git)-[main]
NAME READY STATUS RESTARTS AGE LABELS
my-app 1/1 Running 0 19s app=MyApp

Check the service created here:

$ kubectl get svc
(git)-[main]
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-service ClusterIP 10.103.56.13 <none> 80/TCP 9s

Now that the Service has been exposed, so we will access the Pod through Service using a port forwarding. The kubectl port-forward allows you to access your application in a cluster from your local machine.

Let’s run the command:

$ kubectl port-forward svc/my-service 8080:80

Then navigate to http://localhost:8080 in a browser and it should look like this:

Nginx page

You can see that this mapped your browser 8080 to the port 80 of Service and access the Nnginx application.

To delete the Service and Pod:

$ kubectl delete -f test-service.yaml
$ kubectl delete -f my-app-pod.yaml

Wrap up

We’ve covered an essential concept and how to use the basic components of Kubernetes. If you want to learn more about it in detail, I recommend you look at the documentation and try the tutorial.

--

--

No responses yet