Nine Kubernetes Tools You Might Not Know

Marvin Beckers

January 27, 2024

Categorized as kubernetes

Everyone working with Kubernetes (mostly likely) has kubectl installed. Most people also have helm. But what other tools are out there for your daily work with Kubernetes and containers? This post explores a couple of projects that range from somewhat known to heavily obscure, but all of them are part of my daily workflows and are my recommendations to aspiring (and seasoned) Kubernetes professionals.

Let’s dive right into our list!

protokol

This one takes the cake as “most obscure” because I am the only person who has starred it on GitHub at the time of writing this. People are seriously missing out.

protokol is a small tool by my friend Christoph (also known as xrstf) that allows you to easily dump Kubernetes pod logs to disk for later analysis. This is especially useful in environments that do not have a logging stack set up that you can query later on. For example, to get all logs from the kube-system namespace until you stop the protokol command (e.g. with Ctrl+C), run:

$ protokol -n kube-system
INFO[Sat, 27 Jan 2024 11:51:47 CET] Storing logs on disk.                         directory=protokol-2024.01.27T11.51.47
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=coredns namespace=kube-system pod=coredns-787d4945fb-4q7jv
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=coredns namespace=kube-system pod=coredns-787d4945fb-ghskz
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=etcd namespace=kube-system pod=etcd-lima-k8s
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=kube-controller-manager namespace=kube-system pod=kube-controller-manager-lima-k8s
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=kube-proxy namespace=kube-system pod=kube-proxy-rppbc
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=kube-scheduler namespace=kube-system pod=kube-scheduler-lima-k8s
INFO[Sat, 27 Jan 2024 11:51:47 CET] Starting to collect logs…                     container=kube-apiserver namespace=kube-system pod=kube-apiserver-lima-k8s
^C
$ tree
.
└── protokol-2024.01.27T11.51.47
    └── kube-system
        ├── coredns-787d4945fb-4q7jv_coredns_008.log
        ├── coredns-787d4945fb-ghskz_coredns_008.log
        ├── etcd-lima-k8s_etcd_008.log
        ├── kube-apiserver-lima-k8s_kube-apiserver_006.log
        ├── kube-controller-manager-lima-k8s_kube-controller-manager_010.log
        ├── kube-proxy-rppbc_kube-proxy_008.log
        └── kube-scheduler-lima-k8s_kube-scheduler_010.log

3 directories, 7 files

protokol comes with a huge set of flags to alter behaviour and to target specific namespaces or pods. It is really useful in troubleshooting situations where you want to grab large parts of the cluster’s current logs, e.g. to grep for certain things. It’s also quite nice in CI/CD systems where logs of pods should be downloaded as artifacts that will be stored alongside the pipeline results.

Tanka

Do you remember ksonnet? No? A lot of people probably don’t. The ksonnet/ksonnet repository was archived in September 2020, which feels like a lifetime ago. ksonnet used to provide Kubernetes-specific tooling based on the jsonnet configuration language, which is basically a way to template and composite JSON data. The generated JSON structures can be Kubernetes objects, which can be converted to YAML or sent to the Kubernetes API directly. In essence, this was an alternative way to distribute your Kubernetes manifests with configuration options.

ksonnet ceased development but Grafana decided to revive the idea with tanka, which was really nice. The unfortunate truth is that jsonnet is very niche, so niche that the syntax highlighting for my blog doesn’t even support it. The only major project outside of Grafana that seems to use jsonnet is kube-prometheus (which doesn’t use tanka, unfortunately).

I personally find the syntax of it great though, much better than Helm doing string templating on YAML. See below for a jsonnet snippet that generates a full Deployment object:

local k = import "k.libsonnet";

{
    grafana: k.apps.v1.deployment.new(
        name="grafana",
        replicas=1,
        containers=[k.core.v1.container.new(
            name="grafana",
            image="grafana/grafana",
        )]
    )
}

You might feel some resistance to introducing tanka to your workplace because it has a learning curve, but once it clicks you never want to go back to helm. If you get buy-in from your colleagues this might be a huge win – The ability to provide standardized libraries to generate manifests can be extremely helpful in providing a consistent baseline to teams. So it might be worth trying it out for your next project.

stalk

Another tool made by xrstf! stalk allows you to observe the changes in Kubernetes resources over time. This can be very useful if you just can’t understand what is happening to your Deployment (or any other resource) if you struggle to observe changes when running kubectl get. Usually, this is most needed when your Kubernetes controller goes into a reconciling loop. stalk to the rescue - it will show diff formatted output with timestamps when changes happen.

In the example below, the observed changes are limited to the .spec field of a Deployment. So stalk will start by showing .spec at start time, and then log any changes it observes over time (in the example, the Deployment has been scaled down to one replica later on):

$ stalk deployment sample-app -s spec
--- (none)
+++ Deployment default/sample-app v371701 (2024-01-27T12:43:10+01:00) (gen. 2)
@@ -0 +1,30 @@
+spec:
+  progressDeadlineSeconds: 600
+  replicas: 2
+  revisionHistoryLimit: 10
+  selector:
+    matchLabels:
+      app: sample-app
+  strategy:
+    rollingUpdate:
+      maxSurge: 25%
+      maxUnavailable: 25%
+    type: RollingUpdate
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: sample-app
+    spec:
+      containers:
+      - image: quay.io/embik/sample-app:latest-arm
+        imagePullPolicy: IfNotPresent
+        name: sample-app
+        resources: {}
+        terminationMessagePath: /dev/termination-log
+        terminationMessagePolicy: File
+      dnsPolicy: ClusterFirst
+      restartPolicy: Always
+      schedulerName: default-scheduler
+      securityContext: {}
+      terminationGracePeriodSeconds: 30

--- Deployment default/sample-app v371701 (2024-01-27T12:43:10+01:00) (gen. 2)
+++ Deployment default/sample-app v371736 (2024-01-27T12:43:13+01:00) (gen. 3)
@@ -1,6 +1,6 @@
 spec:
   progressDeadlineSeconds: 600
-  replicas: 2
+  replicas: 1
   revisionHistoryLimit: 10
   selector:
     matchLabels:

No more head scratching when two controllers compete on specific fields and update them several times a second.

Inspektor Gadget

If the tools in this blog form a toolbox, Inspektor Gadget is the toolbox in the toolbox. For someone with a sysadmin background (like me) this is a treasure trove when troubleshooting low-level issues. The various small tools in Inspektor Gadget are called – unsurprisingly – gadgets and are based on eBPF. You can even write your own gadgets!

Inspektor Gadget consists of a client component (which is a kubectl plugin) and a server component, which runs as DaemonSet on each Kubernetes node (after installing it).

In its essence gadgets give you access to system data you could also fetch from a Kubernetes node’s shell via SSH, but Inspektor Gadget allows to fetch and process this data with the context of containers and across nodes. To just show two of the many available gadgets, below is a snapshot of active sockets in all pods in the current namespace (which usually would be much more):

$ kubectl gadget snapshot socket
K8S.NODE                 K8S.NAMESPACE            K8S.POD                  PROTOCOL SRC                            DST                            STATUS
lima-k8s                 default                  sample-app-6…bf695-4cv89 TCP      r/:::8080                      r/:::0                         LISTEN

The tracing gadgets are also amazing to understand what is actually happening in pods over time. If you want to see which DNS requests and responses happen, you can just use the trace dns gadget:

$ kubectl gadget trace dns
K8S.NODE             K8S.NAMESPACE        K8S.POD              PID         TID         COMM       QR TYPE      QTYPE      NAME                RCODE      NUMA…
lima-k8s             default              sample-app…695-4cv89 98533       98533       ping       Q  OUTGOING  A          google.com.                    0
lima-k8s             default              sample-app…695-4cv89 98533       98533       ping       Q  OUTGOING  AAAA       google.com.                    0
lima-k8s             default              sample-app…695-4cv89 98533       98533       ping       R  HOST      A          google.com.         NoError    1
lima-k8s             default              sample-app…695-4cv89 98533       98533       ping       R  HOST      AAAA       google.com.         NoError    0

Seriously, it’s impossible to overstate how much information in an ongoing incident or a situational analysis can be discovered with Inspektor Gadget. If you operate Kubernetes clusters it should be in your go-to toolbox.

skopeo

This one has the most GitHub stars on the list so it is statistically the tool most people are familiar with, but it still made sense to include it on the list due to its sheer usefulness.

skopeo is strictly speaking not a tool for Kubernetes either – it’s for interacting with container images without the need for a fully blown container runtime running (which is extremely useful on systems that don’t run docker natively, like macOS or Windows). It can assist in both discovering image metadata or manipulating images in various ways.

The two frequent options in daily workflows are likely skopeo copy and skopeo inspect. Here’s an example of inspecting the metadata of an image in a remote registry:

$ skopeo inspect docker://quay.io/embik/sample-app:v0.1.0
{
    "Name": "quay.io/embik/sample-app",
    "Digest": "sha256:efbbf29b92bd8fca3e751c1070ba5bf0f2af31983bfc9b007c7bf26681c59b4c",
    "RepoTags": [
        "v0.1.0"
    ],
    "Created": "2023-04-07T11:38:28.791201794Z",
    "DockerVersion": "",
    "Labels": {
        "maintainer": "marvin@kubermatic.com"
    },
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:91d30c5bc19582de1415b18f1ec5bcbf52a558b62cf6cc201c9669df9f748c22",
        "sha256:565a1b6d716dd3c4fdf123298b33e1b3e87525cff1bdb0da54c47f70cb427727"
    ],
    "LayersData": [
        {
            "MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "Digest": "sha256:91d30c5bc19582de1415b18f1ec5bcbf52a558b62cf6cc201c9669df9f748c22",
            "Size": 2807803,
            "Annotations": null
        },
        {
            "MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "Digest": "sha256:565a1b6d716dd3c4fdf123298b33e1b3e87525cff1bdb0da54c47f70cb427727",
            "Size": 3189012,
            "Annotations": null
        }
    ],
    "Env": [
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ]
}

fubectl

fubectl is a collection of handy aliases for your shell so you don’t have to type out kubectl commands all the time. While this is a project hosted by my current employer, I’ve been using it since before joining Kubermatic.

fubectl is a bit hard to show off in a blog post – the repository README does a much better job at that. Besides the obvious aliases (k instead of kubectl, kall instead of kubectl get pods -A, etc) it does a great job at integrating fuzzy finding via fzf. It makes interacting with Kubernetes much more interactive.

It is much easier to get logs for a pod by running klog and then searching for the pod by typing fragments of its name, and then going through the second stage of selecting the right container within that pod. In a similar fashion, kcns and kcs help switching between contexts and namespaces without much friction.

Once the various aliases of fubectl are in your muscle memory, you can never got back to running kubectl config get-contexts and kubectl config use-context <context> instead of kcs.

kube-api.ninja

This completes the xrstf trifecta of Kubernetes tools you should know about. The difference to all other tools on this list is that this is not a command line tool but a website. It tracks Kubernetes API changes over time in an easy to read table view.

When was a specific resource in a specific API version added to Kubernetes? When was it migrated to another API version? What important API changes are in a specific Kubernetes version (e.g. what APIs might need to be updated in your manifests before upgrading to this Kubernetes version)? kube-api.ninja answers all those questions and many more.

For example the notable API changes for Kubernetes 1.29, showing that some resource types got removed with that version:

kube-api.ninja notable changes for Kubernetes 1.29

kube-api.ninja is also helpful if you are interested in the evolution of APIs. Did you know that HorizontalPodAutoscalers existed as a resource type before Deployments? These days the Kubernetes APIs have stabilized a bit, but I wish I had this around during the extensions to apps migration days.

kubeconform

The last (serious) entry on this list is kubeconform, which is extremely helpful in validating your Kubernetes manifests before applying them. It works great in tandem with helm by first rendering your Helm chart into YAML and then passing that to kubeconform to check for semantic correctness. This is how a simple CI pipeline will much improve your Helm chart’s development process:

helm template \
  --debug \
  name path/to/helm/chart | tee bundle.yaml
# run kubeconform on template output to validate Kubernetes resources.
# the external schema-location allows us to validate resources for
# common CRDs (e.g. cert-manager resources).
kubeconform \
  -schema-location default \
  -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
  -strict \
  -summary \
  bundle.yaml

This will help ensure that PRs changing your Helm chart still produce semantically valid Kubernetes resources, not just valid YAML.

kubeconfig-bikeshed

Okay, okay, okay. This one is shameless self-promotion, so I’ll keep it short. If you struggle with juggling access to many Kubernetes clusters and you feel like multiple contexts in your kubeconfig no longer cut it, kubeconfig-bikeshed (kbs) might be for you. I’ve started writing it to replace my various shell snippets that I was using to manage access to Kubernetes clusters.

How many of the listed tools did you know already? Hopefully you found some new things to try out in your next troubleshooting session or CI/CD pipeline design.

Bikeshedding Kubeconfig Management

Marvin Beckers

November 21, 2023

Categorized as kubernetes

When working on a platform like the Kubermatic Kubernetes Platform (KKP), plenty of kubeconfigs end up in your home directory. Some of them long-lived, some already outdated 30 minutes later.

Previously, I have been handling this with a couple of shell aliases and an occasional rm -rf ~/Downloads/kubeconfig-*. But I would not be a software engineer if I hadn’t been obsessed with optimizing my personal workflows. There is a good reason the profession likes to bikeshed some things to death. And we like to have, uhm, … special workflows.

Since I am no exception to all of that, I decided to write my own application to manage kubeconfigs. Enter kubeconfig-bikeshed – shorthand kbs. My way of keeping my bikes kubeconfigs organized and out of the rain (so they don’t rust – you want to guess what programming language it is written in?)

Let’s take a look at kbs in its current state, where I want it to go and why I want to work on it. Right now, the project is in its infancy, but I hope to make it actually useful in the future.

What is kbs?

The initial v0.1 version comes with admittely a very slim feature set. There are essentially three commands in kbs (ignoring help and shell completion related ones):

  • kbs import takes the path to a kubeconfig anywhere on your system (for example in ~/Downloads) and copies it to a central “store” that kbs is using. That store is in ~/.config/kbs by default, but respects XDG_CONFIG_HOME. While importing, the kubeconfig and context names get overridden with the server name. This is one of the things I kept doing when downloading kubeconfigs, and I often found the FQDN that is hosting the Kubernetes API to be the most “accurate” descriptor of a kubeconfig. If that is not suitable, the --name flag allows to set a name explicitly.
  • kbs list lists the available kubeconfigs from the “store”. It is not very useful on its own, but mostly for scripting purposes and in combination with the next command.
  • kbs use, which prints a shell snippet to export a kubeconfig from the store by name to the KUBECONFIG environment variable.

At the moment, the tool is restricted to import “simple” kubeconfigs, which means they can only have one cluster and one user entry. In the future I hope to improve on this.

This is the baseline for my tool, and thus the v0.1 release. In coming releases, I want to add a boatload of features:

  • flags to change behavior of commands (e.g. --short on kbs import to just use the first portion of the server FQDN as name).
  • let kbs use remember the last active kubeconfig and add kbs use - to “restore” the last selected kubeconfig.
  • add a command to fetch kubeconfigs from remote systems (e.g. for a user cluster in KKP).
  • add a command to “prune” outdated kubeconfigs. The semantics of that are still not fully settled in my head, but I can see it trying to establish a HTTPS connection to the Kubernetes API to decide if the server still exists.
  • a way to manually clean up kubeconfigs (without going to ~/.config/kbs manually).
  • perhaps a way to attach metadata to kubeconfigs when importing them. This could be useful to categorize and filter kubeconfigs, e.g. exclude any kubeconfig that has a vpn=internal flag from being pruned.

While writing this section, new ideas come up every second. There is a lot of potential in kbs that I hope to explore.

Installation

kbs can be installed via brew (if you have Homebrew) or cargo (if you are on Linux and have a working Rust toolchain), depending on what system you are on. For brew, installation is as simple as:

$ brew tap embik/tap
$ brew install kubeconfig-bikeshed

Getting the tap up and running was relatively simple, but writing the formula for a Rust binary was surprisingly difficult to get right. When you search for that online, you run into tons of outdated advice. Maybe this should be the topic for another blog post once I feel more comfortable with it.

With cargo, it is even simpler after pushing my first crate to crates.io:

$ cargo install kubeconfig-bikeshed

After installation, I highly recommend to set up shell integration. For example for zsh, add the following snippet to your .zshrc:

# load kubeconfig-bikeshed shell completion & magic
if command -v kbs &>/dev/null
then
    source <(kbs shell completion zsh)
    source <(kbs shell magic zsh)
fi

But wait … what is kbs shell magic zsh?

Shell magic

One of the more interesting bits in kbs is the optional shell “magic”. It allows the user to run kbs to select a kubeconfig and set it for further use with e.g. kubectl. For zsh, it is loaded in the snippet above. For bash, it can be loaded via kbs shell magic bash.

As with all good magic, the actual trick is very simple. This defines a function called kbs in bash:

alias _inline_fzf="fzf --height=50% --reverse -0 --inline-info --border"
alias _kbs_bin="$(type -p kbs)"

function kbs() {
    if [ $# -eq 0 ]; then
        # if no parameters are passed, we want to run fzf on available kubeconfigs and set the selected one as active kubeconfig
        eval "$(_kbs_bin use $(_kbs_bin ls | _inline_fzf))"
    else
        # if parameters are passed, we just call the kbs binary directly
        ${_kbs_bin} $@
    fi
}

This acts like a shim between the user and the actual kbs binary. This is necessary for actually setting the KUBECONFIG environment variable, which feels like a core functionality of any good kubeconfig manager. You cannot set environment variables from within an application, so this function runs kbs ls to list available kubeconfigs, pipes them through fzf so the user can interactively search and select one, and then exports the full path as KUBECONFIG (kbs use prints something like export KUBECONFIG=path/to/kubeconfig that is going through eval and applied to the current shell).

This only happens when kbs (the shell function now) is called without arguments. if it is called with any arguments, they are passed through to the kbs binary.

The neat trick I learned while working on this is the use of proper shell built-ins. which is the much more popular command to determine where and if a command exists. However, since this is overriding the kbs command with a function, it needs to know where the kbs binary is. type -p does exactly that by simply looking for binaries, and not checking the shell itself.

Note that while type is a unix standard built-in, it works differently across shells. The zsh magic for example uses whence instead of type, because type prints not only the path:

$ type -p kbs
kbs is /opt/homebrew/bin/kbs

Since shell magic is per shell, I can get away with not using type where it doesn’t suit what I need.

Why write your own

Because it is fun. No, seriously. Working in an established ecosystem like Cloud Native can be a challenge sometimes, even though it is a relatively young space. Do not see this as a complaint – Collaboration, discussions, guard rails and architecture considerations are vital to good software. And I enjoy being involved in them.

But writing my own tools can be refreshing. They can do exactly what I want them to do, scratching that super specific itch I have. If people feel the same way I do about those itches, they are very welcome to use kbs. If they feel it doesn’t scratch their itch, they can use another tool or write their own. Or fork kbs. Finding a personal workflow that works has many ways.

Our industry is split on “Not Invented Here” (NIH) syndrome. Some organizations extensively suffer from it, while others are avoiding it – with mostly good reasons – like the plague. I believe collaboration between people with diverse backgrounds and corporate culture always produces the best outcome and should be the default choice over building it on your own. But I think if I can get away with NIH anywhere, it is tools used in personal workflows.

Learning by doing

The way I learn – and I assume most do – is by doing. Rust is one of those programming languages. The ones I have been aware of for years, but never really got into over all those years. The first time I tried to get into Rust was with nightshift, a tool to reduce blue light emissions when using sway as your window manager on Linux. That repository was created six years ago! And it never really got anywhere because I struggled with the language a lot. Since then I have used Rust for a small tool called ing-csv-importer which frankly speaking could have been a shell script.

But I still wanted to learn Rust! And I was bothered by my kubeconfig (mis-)management. The obvious solution to both problems was to combine them. Since I had a rough idea of what the tool should be capable of, the Rust learning process was much more guided. I knew what to look for. The code itself is probably terrible to experienced Rustaceans, but it works and I have already learned a lot about Rust from it. I hope to refine it in the future.

kbs is a “low impact” learning project. I had the idea back in May of 2023. Set up the repository, pulled in some dependencies and called it a day. But since there was no pressure to come back to it, I was able to take my time and get back into it once I was motivated. No one cared if I delivered “on time”, and so I was able to get back into it half a year later.

And after a couple of days of ramp up, I have to say I am delighted with Rust. The language has a certain elegance to it that I cannot explain. But I enjoy concepts like match that make for really nice code. Consider for example the snippet below, which determines the name variable used for a kubeconfig and its context depending on whether the --name flag is set:

let name: String = match matches.get_one::<String>("name") {
    Some(str) => str.clone(),
    None => {
        let hostname = kubeconfig::get_hostname(&kubeconfig)?;
        let url = Url::parse(hostname.as_str())?;
        let host = url
            .host_str()
            .ok_or("failed to parse host from server URL")?;
        host.to_string()
    }
};

This feels so much less clunky to me than an if block in Go, for example. Is that “factually” correct? Maybe not. But Rust has a lot of concepts that I never knew I missed from other languages and I expect to do more work with it in the future.

Closing thoughts

Does kbs do something special? Absolutely not. If people are looking for a mature kubeconfig management solution, they are – at the moment, and maybe forever – better off with konf-go. But the work on this is a lot of fun and gives a certain feeling of self-empowerement once I had “finished” the v0.1.0 release end-to-end (from first commit to available Homebrew package).

Now I am excited to integrate kbs into my daily workflow and improve it where necessary. I plan to maintain it as long as I am working with masses of kubeconfigs, which I sincerely hope won’t change in the near future.

Anyways, I wish some people find use in kbs or the inspiration to start their own learning projects (or not; enjoy your free time with whatever makes you happy). Oh, and regarding that list of programming languages I want to get into – I’m coming for you next, Elixir and/or Gleam (once I get kbs where I want it to be, but … details, am I right?)