Learning Kubernetes with Minikube is useful for understanding concepts, but it can hide many of the steps involved in setting up a real cluster. Working with actual virtual machines provides a better understanding of how Kubernetes is deployed in production environments.
For this demonstration, we use Amazon EC2 instances to build a Kubernetes cluster.
- 1 Control Plane Node
- 1 or more Worker Nodes
-
Provision EC2 instances for the control plane and worker nodes.
-
Install the required Kubernetes components on all nodes:
- containerd (container runtime)
- kubeadm
- kubelet
- kubectl (typically installed on the control plane)
-
Initialize the Kubernetes cluster on the control plane node:
kubeadm init
There are two common ways to create a Deployment in Kubernetes:
- Imperative Approach (Commands)
- Declarative Approach (YAML Files)
The imperative approach is useful for learning and quick testing, while the declarative approach is the standard method used in production environments.
The simplest way to create a Deployment is by using a command.
kubectl create deployment nginx --image=nginx:latestkubectl get deployments
kubectl get pods
kubectl get pods -o widekubectl scale deployment nginx --replicas=3kubectl expose deployment nginx \
--type=NodePort \
--port=80As applications become more complex, additional configuration is required:
- Multiple replicas
- Labels and selectors
- Resource requests and limits
- Environment variables
- Volumes
- Health checks
- Service configuration
Managing all of these options through commands becomes difficult and hard to maintain.
Instead of passing many command-line options, Kubernetes resources are defined in YAML files.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80kubectl apply -f deployment.yamlkubectl get deployments
kubectl get podskubectl describe deployment nginxModify the deployment.yaml file.
For example:
spec:
replicas: 5Apply the changes:
kubectl apply -f deployment.yamlVerify the update:
kubectl get deploymentsDelete using the YAML file:
kubectl delete -f deployment.yamlOr delete directly:
kubectl delete deployment nginxYAML files provide several advantages:
- Easy to read and maintain
- Can be stored in Git repositories
- Supports version control
- Easy collaboration among team members
- Reproducible deployments
- Works well with CI/CD pipelines
- Industry-standard approach for Kubernetes deployments
For small experiments, imperative commands are sufficient. For real-world Kubernetes environments, YAML files are the preferred approach because they are maintainable, reusable, and version controlled.
kubectl get nodeskubectl cluster-infokubectl get allkubectl get nskubectl get pods -Akubectl get podskubectl get pods -n kube-systemkubeadm token create --print-join-commandkubeadm join <CONTROL_PLANE_IP>:6443 \
--token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH>kubectl get pods
kubectl get deployments
kubectl get replicasets
kubectl get serviceskubectl get po # Pods
kubectl get deploy # Deployments
kubectl get rs # ReplicaSets
kubectl get ns # Namespaces
kubectl get svc # Serviceskubectl logs <POD_NAME>kubectl describe <RESOURCE_TYPE> <RESOURCE_NAME>kubectl exec -it <POD_NAME> -- <COMMAND>Example:
kubectl exec -it nginx-pod -- /bin/bashkubectl logs <POD_NAME> -n <NAMESPACE>kubectl describe <RESOURCE_TYPE> <RESOURCE_NAME> -n <NAMESPACE>kubectl exec -it <POD_NAME> -n <NAMESPACE> -- <COMMAND>kubectl create <RESOURCE_TYPE> <RESOURCE_NAME> --image=<IMAGE_NAME>:<TAG>Example:
kubectl create deployment nginx --image=nginx:latestkubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yamlkubectl get <RESOURCE_TYPE>
kubectl get <RESOURCE_TYPE> <RESOURCE_NAME>kubectl edit <RESOURCE_TYPE> <RESOURCE_NAME>Example:
kubectl edit deployment nginxkubectl apply -f deployment.yamlkubectl delete <RESOURCE_TYPE> <RESOURCE_NAME>Example:
kubectl delete deployment nginxThe kuberenete configuration file uses YAML syntax for defining resources. YAML is a key-value pair format that is easy to read and write.
Here is an example of a YAML configuration file for a deployment:
key: value
# when we have objects with multiple fields
key:
subkey: value
# when we have objects with multiple fields and subfields
key:
subkey:
subsubkey: value
# when we have a list
key:
- item1
- item2
- item3
# when we have a list of objects with multiple fields
key:
- subkey: value
subsubkey: value
- subkey: value
subsubkey: value
# when we have a list of objects with multiple fields and subfields
key:
- subkey:
subsubkey: value
subsubkey: value
- subkey:
subsubkey: value
subsubkey: valueYou can use this url to learn more about YAML syntax: https://onlineyamltools.com/convert-yaml-to-json this will show you the syntax implementation by converting YAML to JSON which most programmers are addopted with.
The configuration file have five section:
1, The apiVersion: the apiVersion section specifies the version of the Kubernetes API you are using. e.g. apiVersion: v1, apiVersion: apps/v1
2, The kind: the kind section specifies the type of object you are creating, such as a Pod, Service, or Deployment. e.g. kind: Pod, kind: Deployment
3, The metadata: the metadata section contains information about the object, such as its name, namespace, and labels
4, The spec: this is where you define the desired state of the object, and it has a structure that depends on the kind of object you are creating
5, The status: this will be auto generated by kubernetes
sudo kubeadm init phase control-plane allsudo kubeadm init phase control-plane apiserversudo kubeadm init \
--control-plane-endpoint "k8s.example.com:6443"kubeadm init phase upload-certs --upload-certskubeadm token create --print-join-commandkubeadm join <LOAD_BALANCER>:6443 \
--token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH> \
--control-plane \
--certificate-key <CERTIFICATE_KEY>sudo kubeadm init \
--control-plane-endpoint "k8s.example.com:6443"
# Generate certificates
kubeadm init phase upload-certs --upload-certs
# Generate join command
kubeadm token create --print-join-command
# Add control-plane flag to join command
--control-plane
# Add certificate key to join command
--certificate-key
# The final join command for control plane
kubeadm join LB:6443 \
--token xxx \
--discovery-token-ca-cert-hash sha256:xxx \
--control-plane \
--certificate-key yyyAlways use an odd number of control plane nodes to maintain etcd quorum.
1 → 3 → 5 → 7
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)sudo tee /etc/kubernetes/encryption-config.yaml > /dev/null <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: ${ENCRYPTION_KEY}
- identity: {}
EOF# Backup the kube-apiserver.yaml manifest
sudo cp \
/etc/kubernetes/manifests/kube-apiserver.yaml \
/etc/kubernetes/manifests/kube-apiserver.yaml.bak
# Add the k8s-config volumeMount to the kube-apiserver manifest
sudo sed -i '/mountPath: \/etc\/kubernetes\/pki/a\
- mountPath: /etc/kubernetes\
name: k8s-config\
readOnly: true' \
/etc/kubernetes/manifests/kube-apiserver.yaml
# Add the k8s-config volume to the kube-apiserver manifest
sudo sed -i '/name: k8s-certs/a\
- hostPath:\
path: /etc/kubernetes\
type: Directory\
name: k8s-config' \
/etc/kubernetes/manifests/kube-apiserver.yaml
# Add API server flag
sudo sed -i '/kube-apiserver/a\ - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml' \
/etc/kubernetes/manifests/kube-apiserver.yaml
# Add flag
grep -q encryption-provider-config \
/etc/kubernetes/manifests/kube-apiserver.yaml || \
sudo sed -i '/- kube-apiserver/a\
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml' \
/etc/kubernetes/manifests/kube-apiserver.yaml
The kubelet will automatically restart the static pod when the manifest changes.
In Kubernetes, you can have multiple contexts. A context is a configuration that points to a specific cluster, user, and optionally a default namespace. Contexts make it easy to switch between environments such as development, staging, and production.
Use the following command to list all available contexts:
kubectl config get-contextsExample output:
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
docker-desktop docker-desktop docker-desktop
* minikube minikube minikube defaultThe asterisk (*) indicates the currently active context.
To display the active context, run:
kubectl config current-contextTo switch to another context, use:
kubectl config use-context <context-name>For example:
kubectl config use-context docker-desktopAfter switching, verify the active context by running:
kubectl config get-contextsYou should see output similar to:
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* docker-desktop docker-desktop docker-desktop
minikube minikube minikube defaultThe * has now moved to docker-desktop, indicating that it is the active context.
If you initialized the control plane with:
sudo kubeadm init \
--pod-network-cidr=192.168.0.0/16 \
--apiserver-cert-extra-sans="$(curl -s ifconfig.me)"the generated API server certificate will include the server's public IP, allowing secure access from your laptop.
On the control plane:
cp /etc/kubernetes/admin.conf ~/aws-cluster.conf
sudo chown $(id -u):$(id -g) ~/aws-cluster.confCopy it to your local machine:
scp ubuntu@<CONTROL_PLANE_PUBLIC_IP>:~/aws-cluster.conf .Open aws-cluster.conf and make sure the server field points to the control plane's public IP:
clusters:
- cluster:
server: https://<CONTROL_PLANE_PUBLIC_IP>:6443Move the file to:
~/.kube/aws-cluster.conf
Move the file to:
%USERPROFILE%\.kube\aws-cluster.conf
cp ~/.kube/config ~/.kube/config.backupCopy-Item "$HOME\.kube\config" "$HOME\.kube\config.backup"export KUBECONFIG="$HOME/.kube/config:$HOME/.kube/aws-cluster.conf"
kubectl config view --flatten > "$HOME/.kube/config.merged"
mv "$HOME/.kube/config.merged" "$HOME/.kube/config"
unset KUBECONFIG$env:KUBECONFIG="$HOME\.kube\config;$HOME\.kube\aws-cluster.conf"
kubectl config view --flatten > "$HOME\.kube\config.merged"
Move-Item -Force "$HOME\.kube\config.merged" "$HOME\.kube\config"
Remove-Item Env:\KUBECONFIGkubectl config rename-context kubernetes-admin@kubernetes awskubectl config use-context awskubectl get nodesIf everything is configured correctly, you should see the nodes in your remote Kubernetes cluster and be able to manage it directly from your local machine.
By default, a container stores data in its writable layer. If the container is recreated, that data is lost. To persist data across restarts, Kubernetes provides different volume mechanisms.
An emptyDir volume is created when a Pod starts and exists for the lifetime of that Pod. It is suitable for temporary files, caching, or sharing data between containers in the same Pod.
volumes:
- name: mongodb-data
emptyDir: {}The volume is mounted into the container using:
volumeMounts:
- name: mongodb-data
mountPath: /data/dbAny data written to /data/db is stored in the emptyDir volume instead of the container's writable layer. However, the data is deleted when the Pod is removed.
A PersistentVolume (PV) represents actual storage available to the cluster. It defines where and how data is physically stored, such as a local disk, network storage, or cloud volume.
A PersistentVolumeClaim (PVC) is a request for storage made by an application. Instead of referencing the storage directly, workloads use the PVC, which is then bound to a suitable PV.
A StorageClass defines how Kubernetes should dynamically provision persistent storage. When a cluster has a StorageClass configured, creating a PVC is often enough because Kubernetes automatically creates and binds a matching PV.
If no StorageClass exists, a matching PV must be created manually before the PVC can be bound.
PersistentVolume (PV)→ The actual storage resource.PersistentVolumeClaim (PVC)→ A request for storage made by an application.StorageClass→ A template that tells Kubernetes how to automatically create PVs.
In clusters without a StorageClass, developers or administrators typically create both the PV and the PVC manually.