kubernetes-101

kubernetes 101 for developers

This is a hands-on tutorial to deploy basic services with kubectl and helm on kubernetes cluster running on local machine.

👉🏼 No cloud account needed.

👉🏼 No Docker desktop needed.

This tutorial is composed on Intel based Macbook, all the commands and processes should be same for any other machine except tool installation commands.

Table of contents


System Setup


  1. install Docker CLI

    brew install docker
    
  2. install hyperkit to be used as docker run time.

    brew install hyperkit
    
  3. install minikube to be used as kubernetes cluster

    brew install minikube
    
  4. configure minikube resources (optional)

    minikube config set cpus 6
    minikube config set memory 12g
    
  5. start minikube

    minikube start  --driver=hyperkit --container-runtime=docker
    

    minikube start

  6. verify minikube

    minikube kubectl get nodes
    

    !minkube get pods](./docs/minikube_get_po.png)

  7. point terminal’s Docker CLI to the Docker instance inside minikube

    eval $(minikube docker-env)
    
  8. Optionally you can open minikube dashboard

    minikube dashboard
    

    minikube dashboard

    It should open a dashboard UI in browser like follow:

    minikube dashboard UI

  9. Install helm

     brew install helm
    
  10. Install Argo CD CLI

    brew install argocd
    

kubectl


Get ready

  1. start minikube

     minikube start  --driver=hyperkit --container-runtime=docker
    
  2. point terminal’s Docker CLI to the Docker instance inside minikube

     eval $(minikube docker-env)
    
  3. build demo app image

     cd apps/demo-api
     docker build -t demo-api .
    

    Build demo

Create a Deployment

  1. Create demo app Deployment

    Create a Deployment that manages a Pod. The Pod runs a Container based on the provided Docker image. We can not set image pull policy on CLI and we want to use the local image, so we will create a simple config file to from CLI itself and add the parameter for image pull policy to “Never”.

    Create a deployment file.

     cd ../ # if not on the root path
     cd kubectl
    
     kubectl create deployment demo-api --image=demo-api:latest --dry-run=client -o yaml > deployment.yaml
    

    build demo api

    Now Modify spec.template.spec.containers in deployment.yaml created.

     spec:
         containers:
         - image: demo-api:latest
         name: demo-api
         resources: {}
    

    and add imagePullPolicy: Never so that it looks like:

     spec:
         containers:
         - image: demo-api:latest
         name: demo-api
         resources: {}
         imagePullPolicy: Never
    

    Apply the config file with kubectl

     kubectl apply -f deployment.yaml
    

    Output is similar to:

     kubectl apply -f deployment.yaml
     deployment.apps/demo-api created
    
  2. View the Deployment:

    kubectl get deployments
    

    The output is similar to:

    NAME       READY   UP-TO-DATE   AVAILABLE   AGE
    demo-api   1/1     1            1           28s
    
  3. View the Pod:

     kubectl get pods
    

    The output is similar to:

     NAME                        READY   STATUS    RESTARTS   AGE
     demo-api-7c7d9f4689-sbkns   1/1     Running   0          100s
    
  4. View cluster events:

     kubectl get events
    
  5. View the kubectl configuration:

     kubectl config view
    

Create a Service

By default, the Pod is only accessible by its internal IP address within the Kubernetes cluster. To make the demo-api Container accessible from outside.

  1. Expose the Pod to the public internet using the kubectl expose command:

     kubectl expose deployment demo-api --type=LoadBalancer --port=3000
    
  2. View the Service you created:

     kubectl get services
    

    The output is similar to:

     NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
     demo-api     LoadBalancer   10.101.236.39   <pending>     3000:30490/TCP   13s
     kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          21s
    
  3. Run the following command to open the app:

     minikube service demo-api
    

    The output is similar to:

     |-----------|----------|-------------|----------------------------|
     | NAMESPACE |   NAME   | TARGET PORT |            URL             |
     |-----------|----------|-------------|----------------------------|
     | default   | demo-api |        3000 | http://192.168.106.3:30490 |
     |-----------|----------|-------------|----------------------------|
     🎉  Opening service default/demo-api in default browser...
    

    It should open the following page in the browser:

    express app demo

Clean up

  1. Now you can clean up the resources you created in your cluster:

     kubectl delete service demo-api
     kubectl delete deployment demo-api
    
  2. Optionally, stop the Minikube virtual machine (VM):

     minikube stop
    
  3. Optionally, delete the Minikube VM:

     minikube delete
    

kustomize


In this we will deploy multiple services. To do so we will be using multiple config files for kubernetes and will manage the config with help of kustomize. This will deploy a proper backend, frontend and db instance to power up a full application stack and use it as we do in production. This example will use the realworld blog app API and UI example apps.

kustomize deployment diagram

We will deploy following services:

  1. blog-api: expressjs backend REST API server.
  2. blog-ui: react UI app being served by a nginx server.
  3. postgres: db for blog-api backend service.

Get ready

  1. start minikube

     minikube start  --driver=hyperkit --container-runtime=docker
    
  2. point terminal’s Docker CLI to the Docker instance inside minikube

     eval $(minikube docker-env)
    
  3. build blog-api app image

     cd apps/realblog/blog-api
     docker build -t blog-api .
    

    Output will be similar to:

     ~/dev/learn/kubernetes î‚° cd apps/realblog/blog-api
     docker build -t blog-api .
     Sending build context to Docker daemon  2.048kB
     Step 1/10 : FROM node:16.15.0
     ---> 9d200cd667d5
     Step 2/10 : RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
     ---> Using cache
     ---> 48145b89be5e
     Step 3/10 : WORKDIR /home/node
     ---> Using cache
     ---> 280c540f8d5c
     Step 4/10 : RUN git clone --depth 1 https://github.com/tanem/express-bookshelf-realworld-example-app.git app
     ---> Using cache
     ---> 34dd1b5c6ded
     Step 5/10 : RUN chown -R node:node /home/node/app
     ---> Using cache
     ---> d95cb606a0de
     Step 6/10 : WORKDIR /home/node/app
     ---> Using cache
     ---> 740cacc09bdd
     Step 7/10 : RUN npm i
     ---> Using cache
     ---> 7b86ce450a87
     Step 8/10 : EXPOSE 3000
     ---> Using cache
     ---> 1b4d3ba26c7b
     Step 9/10 : USER node
     ---> Using cache
     ---> 06c0b457b04a
     Step 10/10 : CMD [ "./bin/start.sh" ]
     ---> Using cache
     ---> 487c135295f6
     Successfully built 487c135295f6
     Successfully tagged blog-api:latest
     ``
    
  4. build blog-ui app image

     cd apps/realblog/blog-ui
     docker build -t blog-ui .
    

    Output will be similar to:

     ~ cd apps/realblog/blog-ui
     ~ docker build -t blog-ui .
     Sending build context to Docker daemon   2.56kB
     Step 1/11 : FROM node:14.19.3 as base
     ---> f3ec39298d1b
     Step 2/11 : RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
     ---> Using cache
     ---> 49cc9653ba42
     Step 3/11 : WORKDIR /home/node
     ---> Using cache
     ---> 8668cbb0d4c8
     Step 4/11 : RUN git clone --depth 1 https://github.com/angelguzmaning/ts-redux-react-realworld-example-app.git app
     ---> Using cache
     ---> 24326deaa442
     Step 5/11 : RUN chown -R node:node /home/node/app
     ---> Using cache
     ---> f0a11bcbee98
     Step 6/11 : WORKDIR /home/node/app
     ---> Using cache
     ---> 7b2bb3a27151
     Step 7/11 : RUN npm i
     ---> Using cache
     ---> bd859e464065
     Step 8/11 : RUN npm run build
     ---> Using cache
     ---> f2249c73479a
     Step 9/11 : USER node
     ---> Using cache
     ---> 9a1326890a11
     Step 10/11 : FROM nginx
     ---> 1403e55ab369
     Step 11/11 : COPY --from=base /home/node/app/build /usr/share/nginx/html
     ---> Using cache
     ---> 29b831cc3064
     Successfully built 29b831cc3064
     Successfully tagged blog-ui:latest
    
  5. enable ingress plugin

    Ref: https://kubernetes.io/docs/concepts/services-networking/ingress/

     minikube addons enable ingress
    
  6. add domain to hosts

    Get minikube IP

     minikube ip
    

    update your machine’s hosts file

     sudo vi /etc/hosts
    

    Add the host realblog.local to the file and point it to minikube IP.

    Hosts should look like:

     127.0.0.1	localhost
     255.255.255.255	broadcasthost
     ::1             localhost
    
     192.168.106.3 realblog.local
    

Deploy apps

  1. apply config with kustomize to kubectl

     cd kustomize/realblog
     kubectl apply -k ./
    

    Output will be similar to:

     service/realblog-api created
     service/realblog-postgres created
     service/realblog-ui created
     persistentvolumeclaim/postgres-pv-claim created
     deployment.apps/realblog-api created
     deployment.apps/realblog-postgres created
     deployment.apps/realblog-ui created
     ingress.networking.k8s.io/realblog-ingress created
    
  2. verify deployment and services

    list deployments

     kubectl get deployments
    

    Output will be similar to:

     kubectl get deployments
     NAME                READY   UP-TO-DATE   AVAILABLE   AGE
     realblog-api        1/1     1            1           2m44s
     realblog-postgres   1/1     1            1           2m44s
     realblog-ui         1/1     1            1           2m44s
    

    list pods

     kubectl get pods
    

    Output will be similar to:

     NAME                                 READY   STATUS    RESTARTS   AGE
     realblog-api-785df47759-plwjw        1/1     Running   0          4m15s
     realblog-postgres-5967b7666c-5t8c4   1/1     Running   0          4m15s
     realblog-ui-6fc444cb95-zqcjt         1/1     Running   0          4m15s	```
    
     list services
     ```shell
     kubectl get services
    

    Output will be similar to:

     NAME                TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
     kubernetes          ClusterIP      10.96.0.1      <none>        443/TCP          12h
     realblog-api        LoadBalancer   10.99.28.2     <pending>     3000:30148/TCP   5m23s
     realblog-postgres   ClusterIP      None           <none>        5432/TCP         5m23s
     realblog-ui         LoadBalancer   10.99.96.128   <pending>     80:32187/TCP     5m23s
    

    list ingress

     kubectl get ingress
    

    Output will be similar to:

     NAME               CLASS   HOSTS            ADDRESS         PORTS   AGE
     realblog-ingress   nginx   realblog.local   192.168.106.3   80      6m21s
    
  3. Use blog app

    open “http://realblog.local” in browser. A blogger site should open. Create a user and add some articles to test the app follow.

    blog home

Clean up

  1. Now you can clean up the resources you created in your cluster:

    Run the following command from kustomize/realblog:

     kubectl delete -k ./
    
  2. Optionally, stop the Minikube virtual machine (VM):

     minikube stop
    
  3. Optionally, delete the Minikube VM:

     minikube delete
    

HELM

We will learn this in two phases, first we will deploy the default nginx app and then our demo-api app, second we will deploy the complete realblog project with db, API and UI services and expose it with the ingress.

Get ready

  1. start minikube

     minikube start  --driver=hyperkit --container-runtime=docker
    
  2. point terminal’s Docker CLI to the Docker instance inside minikube

     eval $(minikube docker-env)
    
  3. build demo-api app image

     cd apps/demo-api
     docker build -t demo-api .
     cd ../../
    

    Create a chart with helm CLI

  4. switch to helm dir

     cd helm
    
  5. create helm chart with helm CLI (default template)

    This command creates a directory “realblog” inside which it pusts charts for a service.

     helm create demo
    
  6. explore helm chart created

     cd demo
     ls 
    

    Result should be similar to:

     Chart.yaml  charts      templates   values.yaml
    
  7. install the chart

     helm install demo .
    

    Results should be similar to:

     NAME: demo
     LAST DEPLOYED: Tue Jan  3 19:46:54 2023
     NAMESPACE: default
     STATUS: deployed
     REVISION: 1
     NOTES:
     1. Get the application URL by running these commands:
         export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=demo,app.kubernetes.io/instance=demo" -o jsonpath="{.items[0].metadata.name}")
         export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
         echo "Visit http://127.0.0.1:8080 to use your application"
         kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT
    
  8. expose the service to access from machine To access the app on http follow on screen instructions or run

     kubectl --namespace default port-forward deployment/demo 8080:80
    

    Result should be similar to:

     Forwarding from 127.0.0.1:8080 -> 80
     Forwarding from [::1]:8080 -> 80
     Handling connection for 8080
     Handling connection for 8080
    
  9. verify on browser

    Open http://localhost:8080 in browser and you should be able to see a nginx welcome screen.

    helm default app

Deploy demo-api app with helm

  1. switch to demo chart created in previous stage

     cd helm/demo		
    
  2. update chart

    Update the image repository and tag to use demo-api image and latest tag in values.yaml file, to do so in the file helm/demo/values.yaml update following:

    image.repository : demo-api

    image.tag : latest

    service.port : 3000

  3. upgrade the deployment

     helm upgrade demo .
    

    Result should be similar to:

     Release "demo" has been upgraded. Happy Helming!
     NAME: demo
     LAST DEPLOYED: Wed Jan  4 12:33:24 2023
     NAMESPACE: default
     STATUS: deployed
     REVISION: 3
     NOTES:
     1. Get the application URL by running these commands:
         export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=demo,app.kubernetes.io/instance=demo" -o jsonpath="{.items[0].metadata.name}")
         export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
         echo "Visit http://127.0.0.1:8080 to use your application"
         kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT
    
  4. expose the service to access from machine To access the app on http follow on screen instructions or run

     kubectl --namespace default port-forward deployment/demo 8080:3000
    

    Result should be similar to:

     Forwarding from 127.0.0.1:8080 -> 3000
     Forwarding from [::1]:8080 -> 3000
     Handling connection for 8080
     Handling connection for 8080
    
  5. verify on browser

    Open http://localhost:8080 in browser and you should be able to see express app.

    helm default app

  6. delete deployments

    Delete the deployent to avoid confusion while trying next/other stages.

     helm uninstall demo --wait
    

    It should return :

     release "demo" uninstalled
    

Deploy realblog stack

  1. start minikube

     minikube start  --driver=hyperkit --container-runtime=docker
    
  2. point terminal’s Docker CLI to the Docker instance inside minikube

     eval $(minikube docker-env)
    
  3. build blog-api app image

     cd apps/realblog/blog-api
     docker build -t blog-api .
    
  4. build blog-ui app image

     cd apps/realblog/blog-ui
     docker build -t blog-ui .
    
  5. switch to helm charts directory for realblog

     cd helm/realblog
    
  6. install charts

     helm install realblog . 
    

    Result should be similar to:

     NAME: realblog
     LAST DEPLOYED: Wed Jan  4 12:47:08 2023
     NAMESPACE: default
     STATUS: deployed
     REVISION: 1
     NOTES:
     1. Get the application URL by running these commands:
         http://realblog.local/
         http://realblog.local/api
    
  7. Add cluster IP to local hosts entry for dns resolution

    Get minikube IP

     minikube ip
    

    update your machine’s hosts file

     sudo vi /etc/hosts
    

    Add the host realblog.local to the file and point it to minikube IP.

    Hosts should look like:

     127.0.0.1	localhost
     255.255.255.255	broadcasthost
     ::1             localhost
    
     192.168.106.3 realblog.local
    

    In this the IP 192.168.106.3 is the IP of cluster node returned by command minikube ip

  8. verify on browser

    Open http://realblog.local/ in browser and you should be able to see blog app UI.

    helm default app

Clean up

  1. Now you can clean up the resources you created in your cluster:

     helm uninstall realblog --wait
    
  2. Optionally, stop the Minikube virtual machine (VM):

     minikube stop
    
  3. Optionally, delete the Minikube VM:

     minikube delete
    

Argo CD

Get ready

  1. start minikube

     minikube start  --driver=hyperkit --container-runtime=docker
    
  2. point terminal’s Docker CLI to the Docker instance inside minikube

     eval $(minikube docker-env)
    
  3. build blog-api app image

     cd apps/realblog/blog-api
     docker build -t blog-api .
    
  4. build blog-ui app image

     cd apps/realblog/blog-ui
     docker build -t blog-ui .
    
  5. add domain to hosts

    Get minikube IP

     minikube ip
    

    update your machine’s hosts file

     sudo vi /etc/hosts
    

    Add the host realblog.local to the file and point it to minikube IP.

    Hosts should look like:

     127.0.0.1	localhost
     255.255.255.255	broadcasthost
     ::1             localhost
    
     192.168.106.3 realblog.local
    

    In this the IP 192.168.106.3 is the IP of cluster node returned by command minikube ip

  6. Run argo CD

     kubectl create namespace argocd
     kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
     kubectl port-forward svc/argocd-server -n argocd 8080:443
    

    Results should be similar to:

     namespace/argocd created
    
     customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
     customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
     customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
     serviceaccount/argocd-application-controller created
     serviceaccount/argocd-applicationset-controller created
     serviceaccount/argocd-dex-server created
     serviceaccount/argocd-notifications-controller created
     serviceaccount/argocd-redis created
     serviceaccount/argocd-repo-server created
     serviceaccount/argocd-server created
     role.rbac.authorization.k8s.io/argocd-application-controller created
     role.rbac.authorization.k8s.io/argocd-applicationset-controller created
     role.rbac.authorization.k8s.io/argocd-dex-server created
     role.rbac.authorization.k8s.io/argocd-notifications-controller created
     role.rbac.authorization.k8s.io/argocd-server created
     clusterrole.rbac.authorization.k8s.io/argocd-application-controller created
     clusterrole.rbac.authorization.k8s.io/argocd-server created
     rolebinding.rbac.authorization.k8s.io/argocd-application-controller created
     rolebinding.rbac.authorization.k8s.io/argocd-applicationset-controller created
     rolebinding.rbac.authorization.k8s.io/argocd-dex-server created
     rolebinding.rbac.authorization.k8s.io/argocd-notifications-controller created
     rolebinding.rbac.authorization.k8s.io/argocd-redis created
     rolebinding.rbac.authorization.k8s.io/argocd-server created
     clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller created
     clusterrolebinding.rbac.authorization.k8s.io/argocd-server created
     configmap/argocd-cm created
     configmap/argocd-cmd-params-cm created
     configmap/argocd-gpg-keys-cm created
     configmap/argocd-notifications-cm created
     configmap/argocd-rbac-cm created
     configmap/argocd-ssh-known-hosts-cm created
     configmap/argocd-tls-certs-cm created
     secret/argocd-notifications-secret created
     secret/argocd-secret created
     service/argocd-applicationset-controller created
     service/argocd-dex-server created
     service/argocd-metrics created
     service/argocd-notifications-controller-metrics created
     service/argocd-redis created
     service/argocd-repo-server created
     service/argocd-server created
     service/argocd-server-metrics created
     deployment.apps/argocd-applicationset-controller created
     deployment.apps/argocd-dex-server created
     deployment.apps/argocd-notifications-controller created
     deployment.apps/argocd-redis created
     deployment.apps/argocd-repo-server created
     deployment.apps/argocd-server created
     statefulset.apps/argocd-application-controller created
     networkpolicy.networking.k8s.io/argocd-application-controller-network-policy created
     networkpolicy.networking.k8s.io/argocd-applicationset-controller-network-policy created
     networkpolicy.networking.k8s.io/argocd-dex-server-network-policy created
     networkpolicy.networking.k8s.io/argocd-notifications-controller-network-policy created
     networkpolicy.networking.k8s.io/argocd-redis-network-policy created
     networkpolicy.networking.k8s.io/argocd-repo-server-network-policy created
     networkpolicy.networking.k8s.io/argocd-server-network-policy created
    
     Forwarding from 127.0.0.1:8080 -> 8080
     Forwarding from [::1]:8080 -> 8080
     Handling connection for 8080
     Handling connection for 8080
    
  7. Use Argo CD UI

    Open https://localhost:8080/ in your browser and you should see login page of the argocd.

    argo login page

    Login with username and password:

    Username is admin and password you can get from following command:

     kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
    

    After login you should see the app explorer page like follow:

    argocd no apps

  8. get CLI ready

    You can create app from UI also but this guide will use CLI to create the app.

    • open a new terminal as the cuurent one is used to port farward argo.
    • Login on argocd CLI
     argocd login localhost:8080
    

    the result should be similar to:

     WARNING: server certificate had error: x509: “Argo CD” certificate is not trusted. Proceed insecurely (y/n)? y
     Username: admin
     Password:
     'admin:login' logged in successfully
     Context 'localhost:8080' updated
    

    use the same username and password used to login on UI.

create argocd app

In this we will use the same helm charts used for deployment of services and images created in set up section to create deployment through argocd.

  1. create app with CLI

     argocd app create realblog --repo https://github.com/satyamyadav/kubernetes-101.git --path helm/realblog --dest-server https://kubernetes.default.svc --dest-namespace default
    
  2. verify on browser

    open https://localhost:8080/applications/argocd/realblog?view=tree&resource=kind%3ADeployment It should show the deployments.

    argo deployments

    Sync the app if it is not synced.

  3. verify app

    open http://realblog.local you should see the blog app UI home page.

    blog home page

Clean up

  1. Now you can clean up the resources you created in your cluster: you can delete the app from argocd UI also.

     argocd app delete realblog
    
  2. Optionally, stop the Minikube virtual machine (VM):

     minikube stop
    
  3. Optionally, delete the Minikube VM:

     minikube delete
    

References