Kubernetes Basics
Learning essential components of Kubernetes
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:
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.
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:
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
.
Node
provides a Kubernetes runtime environment and allows you to run an application, which means that Node has the actual state of your application.
The kubelet
on each Node stores the actual state into the etcd
through the API.
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.
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.
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.
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.
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.
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.
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.
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:
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.