— Tutorial, CKAD, Kubernetes, K8s — 8 min read
This post is just notes to myself for my CKAD exam prep
Linux Foundation and CNCF together prepare CKAD exam
DEVOPS15 gets you 15% discount when registering for the exam
Node is a single machine, that can have multiple pods
Cluster is the combination of multiple machines, or nodes
Master node is configured to manage cluster of machines
API Server: the front end of the cluster
etcd: store the data to manage the cluster, key:value pairs
Scheduler: distributing works across the cluster
Controller: brain of the cluster, makes decision to run containers
Container runtime: in this case docker
kubelet: agents running in the nodes
Worker node is where containers are hosted
We need container runtime to execute container processes
Pod is the simplest form of K8s object
Pod has 1 to 1 relationship between Pod and Container, thus to scale up, increase pods
However, if there is a helper container, it can be deployed to the pod too
Remember, aim is to build replicas that can be increased/decreased as necessary
To find the Nginx image from dockerhub and run in a pod
$ kubectl run nginx --image nginx
then get the pods running
$ kubectl get pods
It is also possible to deploy a pod via yaml file
apiVersion: v1kind: Pod metadata: name:myapp-pod label: app: myapp type: front-end annotation:spec: containers: - name: my-container-name-definition image: nginx
Metadata children are name and labels
To create a redisapp pod with redis image imperative way
$ kubectl run redis --image=redis --generator=run-pod/v1
For more info on generators: https://kubernetes.io/docs/reference/kubectl/conventions/#generators
$ kubectl get pod <pod-name> -o yaml > pod-definition.yaml
Important: you have to delete and re-create the pod
$ kubectl delete pod <pod-name>$ kubectl apply -f pod-definition.yaml
$ kubectl edit pod <pod-name>
We need replication controller to manage the desired state and scaling
Replication controller is the old version of the Replica Sets
Skeleton yaml file below for Replication Controller, not complete
apiVersion: v1kind: ReplicationControllermetadata:spec: template: ... #Pod definition here, without apiVersion and kind replicas: 3
A simple version
apiVersionkind: ReplicationControllermetadata: name: myapp-rc labels: app: myapp type: front-endspec: template: metadata: name: myapp labels: app:myapp spec: containers: - name: nginx-controller image: nginx replicas: 3
Now let's look at a replicaset definition:
apiVersion: apps/v1kind: ReplicaSetmetadata: name: myapp-rs labels: app: myapp type: front-endspec: template: metadata: name: myapp labels: app:myapp spec: containers: - name: nginx-controller image: nginx replicas: 2 selector: matchLabels: app:myapp
Note that replicaset requires selectors, unlike replica controller
$ kubectl replace -f replicaset-manifest.yaml
where in the replicaset-manifest.yaml file we updated the replicas
another way is to just update the file and use apply command
$ kubectl apply -f replicaset-manifest.yaml
the imperative ways of doing the same:
$ kubectl scale --replicas=3 rs/myapp#OR$ kubectl scale --replicas=3 -f replicaset-manifest.yaml #OR $ kubectl scale --replicas=3 replicaset name=myapp
the code above assumes that repliaset is different than 3, and the replicaset name is myapp
Create a deployment and scale it up, imperatively
$ kubectl create deployment webapp --image=kodekloud/webapp-color$ kubectl scale deployment/webapp --replicas=3
then expose the deployment created, containers listen on 8080 and expose the service port 30082, expose as NodePort
$ kubectl expose deployment webapp --type=NodePort --port=30082 --target-port=8080 --name=webapp-service
Just a few tricks for the exam: use --dry-run=client and -o yaml options to try if the code will work, instead of waiting pods to come online
to create a namespace, you can use a yaml file with namespace in the metadata
you can also do it imperatively by creating a ns, $ kubectl create ns namespace-name
to refer to other pods in another namespace, just append --namespace flag to the end
to create pods in an another namespace, you can also use namespace tag under the metadata, similar to name, label, annotations
to switch, use set-context under the config
$ kubectl config set-context mycurrentnamespace --namespace=myothernamespace
if you don't remember your current context or don't want to look it up
$ kubectl config set-context $(kubectl config current-context) --namespace=SwitchToThisNamespace
apiVersion: v1kind: ResourceQuotametadata: name: compute-quota namespace: devspec: hard: pods: "10" requests.cpu: "4" requests.memory: 5Gi limits.cpu: "10" limits.memory: 10Gi
Docker Commands
To run a container use
$ docker run <image-name>
docker build from dockerfile
1FROM busybox2CMD ["Sleep", "5"]
Then use docker build function to run the Ubuntu container The Ubuntu container will run and exit after 5 seconds
What if you want to run the container but sleep after 10 seconds, but wanted to keep the default option as 5 seconds
1FROM busybox2ENTRYPOINT ["Sleep"]3CMD ["5"]
$ docker build -t mybusybox .$ docker run --name mybboxcontainer mybusybox 10
How can you pass these arguments via kubernetes yaml?
1apiVersion: v12kind: Pod3metadata:4 name: busybox 5spec:6 containers:7 - name: busybox-container8 image: mybusybox9 args: ["10"]
note that the image name must match our docker build image tag, pushed to the dockerhub so we can conclude that args overrides CMD
what if I want to override the ENTRYPOINT?, then add command above args
1apiVersion: v12kind: Pod3metadata:4 name: busybox 5spec:6 containers:7 - name: busybox-container8 image: mybusybox9 command: ["sleep"]10 args: ["10"]
how to pass environment variables to pods?
1...2env:
name: APP_COLOR value: pink
1Note that the environment variables are arrays, hence the dash 2Structure must be name/value pairs
Also possible to pass information via configMaps and Secrets
1...2env: 3 - name: APP_COLOR4 valueFrom: 5 configMapKeyRef:67 - name: APP_SECRET8 valueFrom:9 secretKeyRef:1011...
Motivation of configMap is to cope with increasing complexity under env tags
As the application grows, managing each env under Pod definition gets more complex, so configMaps are to rescue
configMap variables are key value pairs and injected to pods
Typical of k8s, we can do it imperatively and declaratively, before injecting to pods
Imperatively:
$ kubectl create configmap configName --from-literal=key=value$ kubectl create configmap configName --from-file=config.filepath
Declaratively:
$ kubectl create -f configmap.yaml
apiVersion: v1kind: configMapmetadata: name: app-config data: key:value anotherkey:anothervalue
apiVersion: v1kind: Pod metadata: name: simple-app labels: env: devspec: containers: - name: containername image: mycontainerimage ports: - containerPort: 8080 envFrom: - configMapRef: name: app-config #Must match configMap name
1#option 12...3envFrom:4 - configMapRef: 5 name: app-config 6...7#option 2 (as single env)8env: 9 - name: CONFIG_KEY10 valueFrom:11 configMapKeyRef:12 name: app-config 13 key: CONFIG_KEY1415#option 316volumes:17 - name: app-config-volume 18 configMap:19 name: app-config
Motivation is similar to configMap but for secret items such as api keys or passwords
Similar to ConfigMaps it can be done imperatively and declaratively
Declaratively, it can be passed as an env:
...spec: containers: - name: myapp envFrom: -secretRef: name: app-secret
apiVersion: v1kind: Secretmetadata: name: app-secretdata: DB_Host: ab12d3
volumes:- name: app-secret-volume secret: secretName: app-secret
the information under secret.yaml are encrypted using base64, but can be decrypted too
Secrets can be injected as env, single env and volumes
Secret volumes mounted to the pod creates individual files
Since secrets are encoded via base64, it can be decoded as well, so it's not that secure
However, if the secrets are not checked into the code repository, can make it safer
Also, enabling encryption at rest for secrets so they are stored encrpyted in /etcd
Secrets are sent to pods if only needed, kubelet stores secrets in tmpfs in memory, not on disk and once that pod is deleted, kubelet will delete its local copy of the secret data as well
imperatively you can create secrets as:
$ kubectl create secret generic secretName --from-literal=key=value
To allow pod to run as non-root, such as userID 1000, just add the securityContext
SecurityContext can also be added to the container level, so that other containers in the pod and the host system are not impacted
apiVersion: v1kind: Podmetadata: name: myPodspec:# Pod level security securityContext: runAsUser: 1000 containers: - name: Ubuntu image: Ubuntu command: ["sleep", "3600"] # Container level security, run as user 1000 instead of root securityContext: runAsUser: 1000 capabilities: add: ["MAC_ADMIN"] #only on the container level
In kubernetes, there are user accounts and service accounts
Humans use user accounts and bots/applications can use service accounts
Service account examples are monitoring, logging, pipeline management
Examples are promatheus (unsure if typed correctly) and jenkins
The applications that interact with my cluster has to be authenticated, for that we use service accounts
to create a service account imperatively
$ kubectl create serviceaccount ServiceAccountName
$ kubectl get serviceaccount
$ kubectl describe ServiceAccountName
When service accounts interact with Kubernetes API, they have to be authenticated
When a service account is created, it also creates a Token, the token is saved in the secret object
Outside the scope of this course, however on a high level the structure for external application to interact with API, you create a service account, assign permissions using role-based control, export the service token to the application to authenticate
What if the third party application itself is hosted in the cluster? Then simply mount the secret as a volume to the pod
To add service account to the pod, just add the following to pod yaml
1apiVersion: v12kind: Pod 3metadata:4 name: my-k8s-dashboard5spec:6 containers:7 - name: mypodname8 image: mypodimage9 serviceAccount: dashboard-sa # the name of the serviceAccount object
Set the pod's CPU, Memory and Disk Requirements
K8s schedules the application to the necessary node
If there is no sufficient space available in the node, then it fails
To set the resource requirements, add the following to the container definition
1apiVersion: v12kind: Pod 3...4spec:5 containers:6 - name: myapp7 image: containerimage8 resources:9 request:10 memory: "1Gi"11 cpu: 0.5
1apiVersion: v12kind: Pod 3...4spec:5 containers:6 - name: myapp7 image: containerimage8 resources:9 requests:10 memory: "1Gi"11 cpu: 0.512 limits:13 memory: "2Gi"14 cpu: 2
The motivation is to set Pod and Node relationship, so selected Pods can / cannot be run on selected Nodes
Tainting is applied to the Node and Toleration is applied to the Pod
Pods without taint toleration cannot be scheduled on the tainted Node
How to taint a node
$ kubectl taint nodes node-name key=value:taint-effect
where taint-effect is what happens to Pods that do not tolerate this taint
there are three taint effects NoSchedule: do not execute, what we have been discussing PreferNoSchedule: try not to schedule here ** NoExecute: do not schedule new and if there is any non-tolerant pods kill the pod (careful pods are gone!)
How to tolerate a pod
$ kubectl taint nodes node1 app=express: NoSchedule--- mypod.yaml apiVersion: v1kind: Podmetadata: name: myserverspec: containers: - name: express-container image: node tolerations: - key: "app" #see app on the taint function above operator: "Equal" # see the equal sign on the taint function above value: "express" effect: "NoSchedule"
Note that toleration to taint does not guarantee that the tolerant Pod will run on the tainted Node, see Affinity Rules for that requirement
For untaint, check $ kubectl taint -h
To see which nodes the pods are running on $ kubectl get pods -o wide
The objective is to select certain Node, i.e. database pod should run on a higher CPU machine
Say you want to add your application to a node that has significant CPU power, thus in your pod manifest you can add nodeSelector command
1apiVersion: v12kind: Pod 3metadata:4 name: my-app5spec:6 containers:7 - name: my-sifnificant-resource-requiring-app 8 image: dockerhub-image-name9 nodeSelector:10 sizeCPU: significantlyhigh
note that nodeSelector key/value pair is something we define, thus we should name the node to match this pod manifest
$ kubectl label nodes NodeName label-key=value-key$ kubectl label nodes myNode sizeCPU=significantlyhigh
Note that the node selectors work for single label and cannot use logical operators, such as select any node where the label is either X or Y, or select any node where the label is NOT equal to X
To select nodes such as above, use Node Affinity
To place a pod to a certain node, you can label the node and use nodeSelector in the Pod spec, such as addition of size=Large
What if you want to create more complicated selection, such as size NOT small or size Medium or Large?
Such complex selection is achieved via Node Affinity on pod definition
1apiVersion: v1 2kind: Pod 3metadata:4 name: nginx 5spec:6 affinity: 7 nodeAffinity:8 requiredDuringSchedulingIgnoredDuringExecution:9 nodeSelectorTerms:10 - matchExpressions:11 - key: disktype12 operator: In 13 values:14 - ssd 15 containers:16 - name: nginx 17 image: nginx 18 imagePullPolicy: IfNotPresent
What if a sneaky engineer updates the node label along the way, will the Pods get deleted?
The answer is under the nodeAffinity line requiredDuringSchedulingIgnoredDuringExecution preferredDuringSchedulingIgnoredDuringExecution ** Planned to be added to K8s: requiredDuringSchedulingRequiredDuringExecution
Most of the time, one pod contains single container
And sometimes (and almost always in distributed systems a.k.a microservices) you want to trace your app logs, track the traffic etc.
However, you don't want to bloat the app with that, so you add another container to the pod that takes care of such app support functions/services, which scales up/down along with app container
There are three patterns: Ambassador, Adapter, Sidecar
To add extra container into pod, just expand the pod spec
1apiVersion: v12kind: Pod 3metadata:4 name: my-app5spec:6 containers:7 - name: my-web8 image: ibmcr-web-app 9 - name: logger 10 image: ibmcr-logger
Readiness probe helps Scheduler to wait before sending the traffic to the newly created pod
That is pretty helpful if you are running a script that creates pods / deployments back to back
1apiVersion: v12kind: Pod3metadata:4 labels:5 test: liveness6 name: liveness-exec7spec:8 containers:9 - name: liveness10 image: k8s.gcr.io/busybox11 args:12 - /bin/sh13 - -c14 - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 60015 #OPTION 1:16 readinessProbe:17 exec:18 command:19 - cat20 - /tmp/healthy21 initialDelaySeconds: 522 periodSeconds: 523 24 OPTION 2:25 readinessProbe:26 httpGet:27 path: /healthz28 port: 808029 httpHeaders:30 - name: Custom-Header31 value: Awesome32 initialDelaySeconds: 333 periodSeconds: 3
Liveliness probe is the same as the readiness probe, however serves for a different reason
Liveliness checks if your container is responding to client requests. Sometimes, your pod can be running and ready but your container is frozen or your code is broken
For YAMLs use livelinessProbe instead of readinessProbe and the rest is the same
$ kubectl logs -f my-pod my-first-container
If the container has stopped, add -c flag, if you have multiple containers add --all-containers=true flag
Kubernetes does not have native metrics monitoring solution, so we use 3rd party such as promatheus or ELK stack. Kubernetes metrics server is also available at github https://github.com/kubernetes-incubator/metrics-server.git
Clone the link and apply the files under /deploy/[metrics-version] folder
Similar to linux performance command 'top' use kubectl top [element] where element is node or pod
Labels help multiple objects to be accessed at once
For replicaSets or deployments, the label for the replicaset can be different from pod spec, the most important line is Selector in the yaml to select the pods
Pod to pod communication can be done through services, because Pods are ephemeral, when pods are deleted the assigned IPs are gone
Services keep track of the pods and can expose the service to outside of cluster
There are 3 type of services, ClusterIP, NodePort and LoadBalancer
ClusterIP is the default choice, it creates a cluster wide IP address that Pods can communicate with
NodePort is ClusterIP + a static port open within the Node, thus all pods are accessible from the port; to access you can ping NodeIP:NodePort
LoadBalancer allows a service to distribute the traffic among the pods in the cluster. Can be run on cloud providers which support LBs.
From the Service object perspective, Service object has a ClusterIP and a Port. Service object connects to a Pod at the TargetPort Node Port opens up a static port with a range between 30000 and 32767 on the Node (i.e machine)
1apiVersion: v1 2kind: Service 3metadata:4 name: myapp-svc 5spec: 6 type: NodePort 7 ports:8 - targetPort: 809 port: 8010 nodePort: 30008 # optional, if not given, auto selected 11 #selector is needed to match the pod label 12 selectors:13 name: myapp
1apiVersion: v1 2kind: Service 3metadata:4 name: myapp-svc 5spec: 6 type: ClusterIP #default option, can be left out 7 ports:8 - targetPort: 80 #9 port: 8010 nodePort: 30008 # optional, if not given, auto selected 11 #selector is needed to match the pod label 12 selectors:13 name: myapp