YouTube Video of Tutorial Walkthrough (macOS)
YouTube Video of Tutorial Walkthrough (Linux)
- Prerequisites
- minikube Prerequisites
- Docker Install
- minikube Install
- Create a minikube Cluster
- Explore the Cluster
- Deploy a Containerized Application in a Pod
- Deploy the Containerized Application in a Deployment
- Use a Service to Expose the Application
- Cleanup
Local Kubernetes cluster:
- 2 CPUs or more
- 2GB of free memory
- 20GB of free disk space
- Internet connection
- Container or virtual machine manager, such as: Docker, QEMU, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox, or VMware Fusion/Workstation
You can install Docker Desktop via appropriate links on https://docs.docker.com/get-docker/.
Note: There are multiple ways to install Docker, the following are several ways for this tutorial.
via Homebrew
brew install --cask docker
open /Applications/Docker.app
- Proceed with the Docker Desktop prompts and wait till Docker Desktop is running
via MacPorts
port install docker
open /Applications/Docker.app
- Proceed with the Docker Desktop prompts and wait till Docker Desktop is running
via Chocolately
choco install docker
via binary
-
Run the Docker installer
wget -qO- https://get.docker.com/ | sh
-
Add user to the docker group
sudo usermod -aG docker $USER
-
Activate changes to the docker group
newgrp docker
-
Verify Docker is running
docker version
-
Add dependencies
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
-
Add the Docker repository
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
-
Update your system
sudo yum update -y
-
Use a package manager to install Docker
sudo yum install docker-ce -y
-
Start the Docker service and enable it to start on boot
sudo systemctl start docker && sudo systemctl enable docker
-
Add user to the docker group
sudo usermod -aG docker $USER
-
Activate changes to the docker group
newgrp docker
-
Verify Docker is running
docker version
Go to the minikube installation page to determine the installation route for your environment: https://minikube.sigs.k8s.io/docs/start/
via Homebrew:
brew install minikube
via Release Binaries for Intel Macs
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64
sudo install minikube-darwin-amd64 /usr/local/bin/minikube
via Release Binaries for Apple Silicon Macs:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64
sudo install minikube-darwin-arm64 /usr/local/bin/minikube
via Chocolately:
choco install minikube
via Windows Package Manager:
winget install minikube
via PowerShell (as Administrator):
New-Item -Path 'c:\' -Name 'minikube' -ItemType Directory -Force
Invoke-WebRequest -OutFile 'c:\minikube\minikube.exe' -Uri 'https://github.com/kubernetes/minikube/releases/latest/download/minikube-windows-amd64.exe' -UseBasicParsing
$oldPath = [Environment]::GetEnvironmentVariable('Path', [EnvironmentVariableTarget]::Machine)
if ($oldPath.Split(';') -inotcontains 'C:\minikube'){
[Environment]::SetEnvironmentVariable('Path', $('{0};C:\minikube' -f $oldPath), [EnvironmentVariableTarget]::Machine)
}
- Reopen the terminal
For AMD64/x86:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
For ARM64:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-arm64
sudo install minikube-linux-arm64 /usr/local/bin/minikube
- Use minikube to create a cluster.
Run:
minikube start
Expected output:
π minikube v1.32.0 on Darwin 13.2 (arm64)
β¨ Automatically selected the docker driver. Other choices: vmware, ssh
π Using Docker Desktop driver with root privileges
π Starting control plane node minikube in cluster minikube
π Pulling base image ...
πΎ Downloading Kubernetes v1.28.3 preload ...
> preloaded-images-k8s-v18-v1...: 341.16 MiB / 341.16 MiB 100.00% 23.79 M
> gcr.io/k8s-minikube/kicbase...: 410.58 MiB / 410.58 MiB 100.00% 27.94 M
π₯ Creating docker container (CPUs=2, Memory=7793MB) ...
π³ Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
βͺ Generating certificates and keys ...
βͺ Booting up control plane ...
βͺ Configuring RBAC rules ...
π Configuring bridge CNI (Container Networking Interface) ...
π Verifying Kubernetes components...
βͺ Using image gcr.io/k8s-minikube/storage-provisioner:v5
π Enabled addons: storage-provisioner, default-storageclass
π Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
- Verify the cluster is up with
kubectl
bundled with minikube:
Run:
minikube kubectl get nodes
NOTE: Minikube downloads kubectl
on the first time using kubectl
bundled with minikube.
Expected output:
> kubectl.sha256: 64 B / 64 B [-------------------------] 100.00% ? p/s 0s
> kubectl: 52.80 MiB / 52.80 MiB [-------------] 100.00% 6.54 MiB p/s 8.3s
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 23m v1.28.3
- Retrieve the Pods in the default namespace, since a namespace is not specified with the
minikube kubectl get pods
commands, thedefault
namespace is used.
Run:
minikube kubectl get pods
Expected output:
No resources found in default namespace.
- Let's take a look at the existing Namespaces. Retrieve all namespaces
minikube kubectl -- get namespaces
Expected output:
default Active 14m
kube-node-lease Active 14m
kube-public Active 14m
kube-system Active 14m
- Since we've seen the existing Namespaces. Let's get Pods from all namespaces
minikube kubectl get pods --all-namespaces
Expected output:
Error: unknown flag: --all-namespaces
See 'minikube kubectl --help' for usage.
Typically with kubectl, kubectl get pods --all-namespaces
works to get Pods from all namespaces but with minikube kubectl
we have to use minikube kubectl -- get pods --all-namespaces
Run:
minikube kubectl -- get pods --all-namespaces
Expected output:
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-5dd5756b68-x6nx5 1/1 Running 0 32m
kube-system etcd-minikube 1/1 Running 0 32m
kube-system kube-apiserver-minikube 1/1 Running 0 32m
kube-system kube-controller-manager-minikube 1/1 Running 0 32m
kube-system kube-proxy-cj8cn 1/1 Running 0 32m
kube-system kube-scheduler-minikube 1/1 Running 0 32m
kube-system storage-provisioner 1/1 Running 1 (31m ago)
- Create a new namespace for our application.
Run:
minikube kubectl create namespace mynamespace
Expected output:
namespace/mynamespace created
- Imperatively, run a Pod with an
nginx
container using thenginx
container image with the1.25.4
tag in themynamespace
namespace and expose port 80 of the Pod.
Run:
minikube kubectl -- run nginx --image nginx:1.25.4 --namespace mynamespace --port 80
Expected output:
pod/nginx created
- Get the virtual IP of the
nginx
Pod.
Run:
minikube kubectl -- get pods --namespace mynamespace -o wide
Expected output:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 55s 10.244.0.3 minikube <none> <none>
NOTE: The IP may be different in your environment
- Check the logs of the nginx Pod
Run:
minikube kubectl -- logs nginx --namespace mynamespace
Expected output:
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2024/02/20 23:11:01 [notice] 1#1: using the "epoll" event method
2024/02/20 23:11:01 [notice] 1#1: nginx/1.25.4
2024/02/20 23:11:01 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2024/02/20 23:11:01 [notice] 1#1: OS: Linux 6.6.12-linuxkit
2024/02/20 23:11:01 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/02/20 23:11:01 [notice] 1#1: start worker processes
2024/02/20 23:11:01 [notice] 1#1: start worker process 29
...
- Delete the nginx Pod
Run:
minikube kubectl -- delete pod nginx -n mynamespace
Expected output:
pod "nginx" deleted
-
That was the imperative (with a command) way to deploy a Pod. Typically, a declarative (via manifests) way to deploy is preffered.
-
Deploy the nginx Pod in a declarative way. Create a manifest for the nginx Pod.
Run:
cat <<EOF > pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: mynamespace
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80
EOF
Use minikube kubectl apply -f
to apply the configuration using the pod.yaml manifest
Run:
minikube kubectl -- apply -f pod.yaml
Expected output:
pod/nginx created
Check the status of the nginx Pod. Run:
minikube kubectl -- get pods -n mynamespace -o wide
Expected output:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 98s 10.244.0.5 minikube <none> <none>
- Delete the nginx Pod to cleanup
Run:
minikube kubectl -- delete pod nginx -n mynamespace
Expected output:
pod "nginx" deleted
- Create a manifest for a Deployment that has 2 replicas of the same Pod using an older version of nginx so that we can update later.
Run:
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: mynamespace
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25.1
ports:
- containerPort: 80
EOF
- Use
minikube kubectl apply
to apply the deploment.yaml manifest to the minikube cluster.
Run:
minikube kubectl -- apply -f deployment.yaml
Expected output:
deployment.apps/nginx created
- Checkout the resources created
Run:
minikube kubectl -- get all -n mynamespace
Expected output:
NAME READY STATUS RESTARTS AGE
pod/nginx-6476dd4bcf-m842c 1/1 Running 0 51s
pod/nginx-6476dd4bcf-xtp2f 1/1 Running 0 51s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 2/2 2 2 51s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-6476dd4bcf 2 2 2 51s
Notice the Deployment has created a ReplicaSet and the ReplicaSet has created the Pods. Let's take a farther look into each resource.
- Describe the nginx Deployment
Run:
minikube kubectl -- describe deployment nginx -n mynamespace
Expected output:
Name: nginx
Namespace: mynamespace
CreationTimestamp: Tue, 20 Feb 2024 18:41:51 -0800
Labels: app=nginx
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=nginx
Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.25.1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: nginx-6476dd4bcf (2/2 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 2m19s deployment-controller Scaled up replica set nginx-6476dd4bcf to 2
Review the fields and note the OldReplicaSets
and the NewReplicaSet
.
There are no OldReplicaSets
and the NewReplicaSet
matches the existing ReplicaSet.
- Describe the ReplicaSet.
Run:
minikube kubectl -- describe rs $(minikube kubectl -- get rs -n mynamespace -o jsonpath='{.items[0].metadata.name}') -n mynamespace
Expected output:
Name: nginx-6476dd4bcf
Namespace: mynamespace
Selector: app=nginx,pod-template-hash=6476dd4bcf
Labels: app=nginx
pod-template-hash=6476dd4bcf
Annotations: deployment.kubernetes.io/desired-replicas: 2
deployment.kubernetes.io/max-replicas: 3
deployment.kubernetes.io/revision: 1
Controlled By: Deployment/nginx
Replicas: 2 current / 2 desired
Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=nginx
pod-template-hash=6476dd4bcf
Containers:
nginx:
Image: nginx:1.25.1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 10m replicaset-controller Created pod: nginx-6476dd4bcf-xtp2f
Normal SuccessfulCreate 10m replicaset-controller Created pod: nginx-6476dd4bcf-m842c
- Update the application to use a newer version of nginx. You can update the deployment.yaml manifest but let's use an imperative command.
Run:
minikube kubectl -- set image deployment/nginx nginx=nginx:1.25.4 -n mynamespace
Expected output:
deployment.apps/nginx image updated
Checkout the resources created
Run:
minikube kubectl -- get all -n mynamespace
Expected output:
NAME READY STATUS RESTARTS AGE
pod/nginx-6476dd4bcf-m842c 1/1 Terminating 0 18m
pod/nginx-6476dd4bcf-xtp2f 1/1 Running 0 18m
pod/nginx-7ffd9c9dbb-rxkdb 0/1 ContainerCreating 0 0s
pod/nginx-7ffd9c9dbb-sxqzc 1/1 Running 0 1s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 2/2 2 2 18m
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-6476dd4bcf 1 1 1 18m
replicaset.apps/nginx-7ffd9c9dbb 2 2 1 1s
Note: If you are fast enough then you will see both ReplicaSets and Pods terminating and creating
- Let's describe the Deployment again.
Run:
minikube kubectl -- describe deployment nginx -n mynamespace
Expected output:
Name: nginx
Namespace: mynamespace
CreationTimestamp: Tue, 20 Feb 2024 18:41:51 -0800
Labels: app=nginx
Annotations: deployment.kubernetes.io/revision: 2
Selector: app=nginx
Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.25.4
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: nginx-6476dd4bcf (0/0 replicas created)
NewReplicaSet: nginx-7ffd9c9dbb (2/2 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 20m deployment-controller Scaled up replica set nginx-6476dd4bcf to 2
Normal ScalingReplicaSet 98s deployment-controller Scaled up replica set nginx-7ffd9c9dbb to 1
Normal ScalingReplicaSet 97s deployment-controller Scaled down replica set nginx-6476dd4bcf to 1 from 2
Normal ScalingReplicaSet 97s deployment-controller Scaled up replica set nginx-7ffd9c9dbb to 2 from 1
Normal ScalingReplicaSet 96s deployment-controller Scaled down replica set nginx-6476dd4bcf to 0 from 1
Now take a look at the OldReplicaSets
and NewReplicaSet
fields.
- Let's scale the Deployment to 3 replicas. There are multiple ways to scale a Deployment, you can update the manifest, update imperatively, or use an Autoscaler like the HorizontalPodAutoscaler
For this excercise, let's use an imperative command to scale the Deployment to 3 replicas.
Run:
minikube kubectl -- scale deployment/nginx --replicas=3 -n mynamespace
Expected output:
deployment.apps/nginx scaled
- Check the Pods of the Deployment.
Run:
minikube kubectl -- get pods -n mynamespace
Expected output:
NAME READY STATUS RESTARTS AGE
nginx-7ffd9c9dbb-k97k5 1/1 Running 0 40s
nginx-7ffd9c9dbb-rxkdb 1/1 Running 0 5m36s
nginx-7ffd9c9dbb-sxqzc 1/1 Running 0 5m37s
Notice the newest Pod.
- Scale the number of replicas of the Deployment to 1.
Run:
minikube kubectl -- scale deployment/nginx --replicas=1 -n mynamespace
Expected output:
deployment.apps/nginx scaled
- We can create a Service imperatively with the
minikube kubectl -- expose
command but let's use a declarative way to create a Service to expose the nginx application. Create the manifest for the Service.
Run:
cat <<EOF > service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: mynamespace
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 8080
targetPort: 80
type: NodePort
EOF
- Apply the service.yaml manifest to the minikubecluster.
Run:
minikube kubectl -- apply -f service.yaml
Expected output:
service/nginx-service created
- Check the nginx-service
Run:
minikube kubectl -- get service -o wide -n mynamespace
Expected output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-service NodePort 10.100.0.73 <none> 8080:30358/TCP 20s app=nginx
- Let's verify the nginx Pod running is the endpoint for the nginx-service.
Run:
minikube kubectl -- get pods -n mynamespace -o wide
Expected output:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-7ffd9c9dbb-sxqzc 1/1 Running 0 15m 10.244.0.8 minikube <none> <none>
Run:
minikube kubectl -- get endpoints -n mynamespace
Expected output:
NAME ENDPOINTS AGE
nginx-service 10.244.0.8:80 83s
Note the Pod's IP and the endpoint of the nginx-service.
- Typically we can reach our Pod on port 30358 of the Node but since this is minikube. Use
minikube kubectl -- port-forward
to forward traffic from localhost port 7080 to the nginx-service.
minikube kubectl -- port-forward service/nginx-service 7080:8080 -n mynamespace &
Expected output:
Forwarding from [::1]:7080 -> 80
Press Ctrl + C to return to the command prompt while port-forwarding runs in the background.
- Try to reach the application with curl (if present)
Run:
curl -sk http://localhost:7080 | head -n 4
Expected output:
Handling connection for 7080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
- Delete all minikube clusters.
Run:
minikube delete --all
Expected output:
π₯ Deleting "minikube" in docker ...
π₯ Removing /Users/<user>/.minikube/machines/minikube ...
π Removed all traces of the "minikube" cluster.
π₯ Successfully deleted all profiles
π Thank you for completing this workshop. You have successfully deployed a containerized app in Kubernetes in multiple ways, updated the app, scaled the app up and down, exposed the app to outside the cluster.