We'll use the following tools:
kubectlkubens(from thekubectxpackage)gcloud
For macOS, you can use brew install kubectl kubectx gcloud.
After installing the tools, we'll need to install some additional plugins to gcloud:
gcloud components install gke-gcloud-auth-pluginUsing a GUI or TUI is quicker than running commands.
- Monokle is a free GUI app that can display cluster resources and edit YAML manifests: https://monokle.io/download.
- k9s is a TUI for viewing and performing operations on cluster resources
Autocompletion simplifies writing the commands in this workshop.
The commands below show how to add it to the current shell, you will have to run it for every new shell opened. For a permanent setup, consult kubectl completion -h.
Bash:
source <(kubectl completion bash)ZSH:
source <(kubectl completion zsh)Fish:
kubectl completion fish | sourcePowershell:
kubectl completion powershell | Out-String | Invoke-Expression-
Go to the Google Cloud Console and login with your account.
-
Run
gcloud auth loginand follow the steps in the browser to login thegcloudCLI. -
Add the Google Kubernetes Engine (GKE) cluster context to the
kubectlconfig:gcloud container clusters get-credentials cloudlabs-25 --region=europe-north1
-
Confirm successful
kubectlsetup by runningkubectl get namespaces. -
An online UI is available at kube-dashboard.cloudlabs-gcp.no. It requires an authentication token. Run
gcloud auth print-access-tokento generate a token, and copy it to online UI to log in.
We'll create a namespace and then deploy a simple app and connect to it, all using kubectl.
-
Create a namespace:
kubectl create namespace <namespace-name>.<namespace-name>should be lowercase with words separated by dashes, e.g.,my-name.Run
kubectl get namespaceto verify that your namespace is in the list. -
Let's start by creating a single pod that runs
podinfo:kubectl run --namespace <namespace-name> <pod-name> --image=stefanprodan/podinfo --port=80You will get a warning about resources, ignore the warning for now.
Tip
Use kubectl <subcommand> --help to get help on any kubectl subcommand. E.g., kubectl run --help.
-
Let's look at the app created. Run
kubectl get --namespace <namespace-name> pods. You should get output looking like:NAME READY UP-TO-DATE AVAILABLE AGE <deployment-name> 2/2 2 2 2m34sYou can view the app information using
kubectl describe --namespace <namespace-name> pod <pod-name>.
Tip
Most kubectl commands support abbreviations of the types. For example, kubectl get pods can be shortened to kubectl get po.
Similarly, flags can be abbreviated. --namespace can be shortened to -n, and --watch can be shortened to -w. So, kubectl get --namespace <namespace-name> pods can be written as kubectl get -n <namespace-name> po.
Feel free to use these abbreviations to save time.
-
Let's change the container image running in the pod to
nginx.- Run
kubectl edit --namespace <namespace-name> pod <pod-name>. This will open the pod manifest in your default text editor. - Find the line with
image: stefanprodan/podinfoand change it toimage: nginx. Save and close the editor. - Open a new shell, and run
kubectl get --namespace <namespace-name> pods --watch. This will show all pods in the namespace, and the--watchflag also print new lines for every update. - Observe the output from the watch command in the second shell. You should see that the pod is terminated and a new pod is created with the updated image.
- Run
-
To access the pod and verify nginx is running, we'll do a port-forwarding from our local computer to the cluster. In your shell, run:
kubectl --namespace <namespace-name> port-forward <pod-name> 8080:80
This will create a tunnel from your local computer on port 8080, to the pod in the cluster on port
80. Open localhost:8080 in your browser to confirm that the app is running smoothly.Use
CTRL-Cor equivalent to cancel the port-forwarding. -
Continue watching the pod updates in the second shell, and run
kubectl delete --namespace <namespace-name> pod <pod-name>. -
Delete the namespace by running
kubectl delete ns <namespace-name>.
Running commands to spin up pods is not ideal. We want to use code to specify our resources.
-
Create a folder called,
podinfo, and put the configuration you create in the following tasks in this folder.For most resource creation commands, you can add
--dry-run=client -o yamlto generate the corresponding YAML configuration. The configuration can then be applied by runningkubectl apply. E.g., to create a namespace like before, runkubectl create namespace <namespace-name> --dry-run=client -o yaml. Then copy the configuration into a code editor, into a file calledpodinfo/namespace.yaml.
Tip
If you're running commands in Bash/Zsh or a similarly compatible you can pipe the configuration directly into a file using >.
E.g., kubectl create namespace <namespace-name> --dry-run=client -o yaml > podinfo/namespace.yaml.
- Run
kubectl apply -R -f podinfo/to apply all manifests in thepodinfo/folder. Verify that the namespace is created, like before.
Tip
We've specified the namespace flag for every command so far. We can simplify the commands by using kubens.
Run kubens <namespace-name>. From now on, you can write commands without --namespace <namespace-name>. Commands in the workshop will explicitly use the argument, even if it's not needed.
-
Similarly, create a manifest for a pod running
nginxinpodinfo/pod.yaml, usingkubectl run --namespace <namespace-name> <pod-name> --image=nginx --port=80 --dry-run=client -o yaml. Apply the manifests usingkubectl apply -R -f podinfo/after creating it. -
Verify that the pod is created and in
Runningstate in the correct namespace.
The following two tasks explore the Monokle GUI and k9s TUI. You can choose to do both, or the one you prefer. Monokle includes more GUI-like features, like creating and editing manifests, while k9s is more focused on viewing and managing cluster resources.
We will edit a file inside the running container, and port-forward to view the changes.
-
Open Monokle, and add a new workspace. Select the
podinfo/folder you created earlier as the workspace folder. Monokle will report some warnings about missing security settings, limits and more, ignore these. -
Connect Monokle to your cluster by clicking the
Connect to Clusterbutton in the top right. Select the correct context for your cluster. Select your namespace to view resources in your namespace. -
Looking at the "Cluster Dashboard". The left side menu shows the workloads and other resources. Click on "Pods" to view the pod you created earlier. Click on the pod. You can see the pod status, logs, and other information in the side panel.
-
Enter the "Shell" tab in the side panel, and execute the command
echo "Hello Monokle world" > /usr/share/nginx/html/index.htmlto change the default nginx page. -
Monokle does not yet support port-forwarding, so run the command:
kubectl port-forward --namespace <namespace-name> <pod-name> 8080:80in your terminal. -
Verify that the text of the page is correct by running
curl localhost:8080or navigating tohttp://localhost:8080in your browser. -
Stop the port-forwarding by pressing
CTRL-Cin your terminal. -
If you prefer, you can use this GUI to edit manifests from now on.
- Open k9s by running
k9sin your terminal.
Tip
Use the command :help or ? in k9s to get help on keybindings.
-
Press
:to start command mode, and typensto list namespaces. Select your namespace by navigating to it and pressingENTER. You can navigate using arrow keys or vim-style keybindings (jork) You can also switch namespaces by pressing0(zero) and selecting the namespace, or by using the command (after:)ns <namespace-name>. -
By default, k9s shows pods. You should see the pod you created earlier. Show the state of the pod by pressing
d("describe"). Go back by pressingESC. -
Press
sto open a shell into the container. Run the commandecho "Hello k9s world" > /usr/share/nginx/html/index.htmlto change the default nginx page. Exit the shell by typingexitand pressingENTER. -
With the pod still selected, press
Shift-fto start port-forwarding. Use80as the container port, but change the local port to8080. PressENTERto start port-forwarding. -
Verify that the text of the page is correct by running
curl localhost:8080or navigating tohttp://localhost:8080in your browser. -
Stop the port-forwarding in k9s by pressing
fto display port-forwards. Select the port-forward and delete it usingctrl-d.
Deployments are a higher-level abstraction that manages Pods. They ensure that a specified number of replicas are running and handle updates to the configuration in predictable ways.
-
Generate a deployment manifest:
kubectl create deployment --namespace <namespace-name> <deployment-name> --image=stefanprodan/podinfo --replicas=2 --port=80 --dry-run=client -o yaml > podinfo/deployment.yaml -
Apply the manifest like before:
kubectl apply -R -f podinfo/ -
Verify using either the CLI, or Monokle/k9s. You should see 1 deployment and 2 pods running. The pods have names prefixed with the deployment name.
- CLI:
kubectl get deployments --namespace <namespace-name>andkubectl get pods --namespace <namespace-name>. - k9s: Go to the namespace using
:ns. View deployments using:deploymentsor:deployto view deployments. View pods using:podsor:po. - Monokle: Select the namespace, and view workloads > deployments and workloads > pods
- CLI:
-
Describe the deployment using either CLI, or Monokle/k9s. Note the port configuration.
- CLI:
kubectl describe deployment <deployment-name> --namespace <namespace-name> - k9s: Select the deployment and press
dto describe. - Monokle: Select the deployment to view its details in the side panel.
- CLI:
-
We used
--port=80in the command to create the deployment, and in the previous step you should have confirmed that theportis configured to be80. However, this port is not exposed by the container. This is documented in the app documentation, but we have no way of knowing otherwise. The correct port is9898. -
Let's edit the deployment.
- CLI: Run
kubectl edit --namespace <namespace-name> deployment <deployment-name>. Find the line withcontainerPort: 80and edit the port number to9898. Save and close, and observe the output fromkubectl get --namespace <namespace-name> pods --watch(or in k9s/Monokle) - k9s: Select the deployment and press
eto edit. Find the line withcontainerPort: 80and edit the port number to9898. Save and close. Navigate to the pods view to observe the pod updates. - Monokle: Select the deployment to view its details in the side panel. Click "Manifest" to view the manifest. Find the line with
containerPort: 80and edit the port number to9898. Save the file by clicking "Update". Navigate to the pods view to observe the pod updates.
- CLI: Run
-
We could also have edited the manifest file
podinfo/deployment.yamldirectly, and reapplied it usingkubectl apply -R -f podinfo/. This is the preferred way of managing resources, as it allows for version control and easier collaboration. Update the manifest to make sure it's not out of sync.
Pods are ephemeral; if they crash or are rescheduled, their IP addresses change. Services provide a stable IP and DNS name to access a set of Pods.
-
Generate a service manifest:
kubectl expose deployment --namespace <namespace-name> <deployment-name> --name=<service-name> --port=80 --target-port=9898 --type=ClusterIP --dry-run=client -o yaml > podinfo/service.yaml -
Apply it:
kubectl apply -f podinfo/service.yaml -
Verify.
- CLI:
kubectl get services --namespace <namespace-name>. - k9s:
:servicesor:svc - Monokle: View services under the Network subheading.
- CLI:
-
Use port-forwarding to access the service. In k9s, find the service and use
shift-flike before. the For the CLI, usekubectl port-forward --namespace <namespace-name> service/<service-name> 9898:80
Note
Take a look at how traffic is routed when refreshing in the browser. Which pod answers the request?
The service normally load balances requests across all pods matching its selector. However, port-forwarding towards a service will always send traffic to the first pod in the list managed by the service.
- Cancel the port-forwarding before proceeding.
In GKE, there are multiple ways to expose services externally: LoadBalancers, Ingresses and Gateway API. We will first try to use LoadBalancers.
-
Update the service to be of type LoadBalancer. You can do this by editing the manifest file
podinfo/service.yaml, changingtype: ClusterIPtotype: LoadBalancer. Then reapply the manifest. -
Look at the updates to the service, and fetch the new field "EXTERNAL-IP". Visit
http://<your-ip>/and see your app in action. Refresh multiple times to see different pods responding.
The following tasks are more open-ended. You will need to look up the documentation to find out how to solve them.
By default, containers have no resource constraints. It is good practice to specify how much CPU and memory (RAM) each container needs.
- Modify your
podinfo/deployment.yamlto add resource requests and limits for the container.- Set memory request to
64Miand limit to128Mi. - Set CPU request to
10m.
- Set memory request to
Tip
See Managing Resources for Pods and Containers for syntax and examples.
- Apply the changes and verify that the pods are recreated with the new configuration. You can check this with
kubectl describe pod <pod-name>.
You can inject configuration into your application using environment variables.
-
Modify
podinfo/deployment.yamlto add an environment variable namedPODINFO_UI_COLORwith the value#ffffff(or any other hex color you like).[!TIP] See Define Environment Variables for a Container.
-
Apply the changes.
-
Visit the external IP of your service again. The UI header color should have changed.
Hardcoding environment variables in the deployment manifest is not always ideal. ConfigMaps allow you to decouple configuration artifacts from image content.
-
Create a new file
podinfo/configmap.yaml. -
Define a ConfigMap named
podinfo-configcontaining the dataui-color: "#34577c". -
Modify
podinfo/deployment.yamlto load thePODINFO_UI_COLORenvironment variable from the ConfigMap instead of defining the value directly.[!TIP] See Configure a Pod to Use a ConfigMap.
-
Apply both files (
kubectl apply -R -f podinfo/). -
Verify that the application still works and uses the configured color.
Secrets are similar to ConfigMaps but are intended to hold sensitive information.
-
Create a new file
podinfo/secret.yaml. -
Define a Secret named
podinfo-secretcontaining a keyapi-keywith some dummy value. -
Modify
podinfo/deployment.yamlto inject this secret as an environment variable namedPODINFO_API_KEY.[!TIP] See Secrets.
-
Apply the changes.
-
Verify the secret is injected by running
kubectl exec -it <pod-name> -- env | grep PODINFO_API_KEY.
When things go wrong, you need tools to investigate.
Logs are the first place to look when an application is behaving unexpectedly.
- View logs for a pod:
kubectl logs <pod-name>. - Follow logs in real-time:
kubectl logs -f <pod-name>. - If a pod has multiple containers, specify the container name:
kubectl logs <pod-name> -c <container-name>.
Tip
In k9s, you can press l to view logs for a selected pod.
Kubernetes emits events when resources change state or when errors occur (e.g., scheduling failures, image pull errors).
- List events in the namespace:
kubectl get events. - Describe a resource to see events related to it:
kubectl describe pod <pod-name>.
Sometimes you need to poke around inside a running container.
- Start an interactive shell:
kubectl exec -it <pod-name> -- /bin/sh(or/bin/bash). - Check file system, environment variables, network connectivity, etc.
Note
Not all containers can run a shell. Some minimal containers may not have a shell installed.
Security is a critical aspect of Kubernetes. By default, pods are quite permissive. Let's lock them down.
You can configure security settings for a Pod or Container using the securityContext field.
- Modify
podinfo/deployment.yamlto add asecurityContextto the container. - Configure the following settings:
- Read-only filesystem:
readOnlyRootFilesystem: true. This prevents the application from writing to the root filesystem. - Disallow privilege escalation:
allowPrivilegeEscalation: false. This prevents the process from gaining more privileges than its parent process. - Run as non-root:
runAsNonRoot: trueandrunAsUser: 1000(or another non-root UID). This ensures the container runs as a regular user. - Drop capabilities: Drop
ALLcapabilities. This removes all default Linux capabilities from the container.
- Read-only filesystem:
- Apply the changes.
- Verify the changes by inspecting the pod:
kubectl get pod <pod-name> -o yaml. - Try to write to the filesystem inside the container:
kubectl exec -it <pod-name> -- touch /tmp/test. This should fail if the filesystem is read-only (note: you might need to mount a volume to/tmpif the app needs to write there, but for this exercise, seeing it fail is the goal).
To cleanup the resources created in this workshop, run:
kubectl delete namespace <namespace-name>