Juggling multiple Kubernetes clusters
If you’re using Kubernetes, chances are that you are interacting with multiple clusters. Maybe even across multiple projects. In this blog I will discuss various strategies of dealing with those clusters from a workflow perspective, helpful tools, and my own personal workflow.
December 3, 2020
productivity
coding
cloud
Over the past few years, Kubernetes has grown from something that ‘seemed very interesting, but also very complex’ to something that you can get as a service from pretty much everyone. Cloud vendors like Amazon, Google, Microsoft, Oracle and DigitalOcean are offering their own flavor of Kubernetes-as-a-Service. Big players in infrastructure like RedHat, Mirantis, Rancher, and Vmware have built Kubernetes offerings for companies that can’t (or don’t want to) use those ‘as a Service’ solutions. Docker Desktop has even shipped with Kubernetes built-in for quite some time now. So basically, Kubernetes is everywhere now and as a result more and more companies are adopting it. (Whether or not they really need to, is something for another blog post)
In this blog post, however, I want to dive into a very common workflow issue: juggling multiple Kubernetes clusters. Or actually, their configs. Over the past few years I’ve worked in environments where I regularly interacted with multiple Kubernetes clusters. A different cluster for staging or production; or even multiple different clusters per environment. And of course my local Docker Desktop K8s, that is used during local development. The issue at hand here is simple: I need to be able to easily run kubectl
commands against different clusters.
Configs and Contexts
Before we dive into juggling strategies, let’s set some definitions. In order for kubectl
(and other tools) to communicate to a Kubernetes cluster, they need configuration. The hostname of the cluster, some form of authentication, and which namespace to use. We call this a ‘context’. You can store this configuration in a kubeconfig
file, and each kubeconfig
can hold multiple contexts.
Approach 1: a single kubeconfig
The first approach, that many will take, is adding that second cluster to the existing default kubeconfig file. Just edit that ~/.kube/config
file and add the bit of YAML for your second cluster, and off you go. You now have 2 contexts. The kubectl
command offers the --context
flag to specify which context to use for your get pods
command, if you don’t specify a context, the default will be used.
## default context
$ kubectl get pods
## any other context
$ kubectl get pods --context foobar
Easy stuff. But you’ll get tired of typing --context foobar
pretty quickly. And occasionally, you’ll forget to add it. So, can we make this easier? Yes! We can switch the default context, like this:
$ kubectl config use-context foobar
Now all subsequent kubectl
commands will use the context foobar
. And you can switch back and forth. To make switching even easier, you can use kubectx, which offers kubectx
for switching contexts, and kubens
for switching namespaces in a cluster:
$ kubectx foobar
$ kubens kube-system
These tools also integrate with the highly recommended fzf, which you should really add to your shell config anyway.
Downsides
So that’s a pretty straightforward approach already, but I can already hear you thinking ‘if this is Approach 1, there’s more coming. What are the downsides?’ Are there downsides? Unfortunately, yes there are. The first downside is that this approach makes it hard to use 2 contexts at the same time. Say that you’re running two terminal windows, and you want to compare your deployments on staging and production, then it would be pretty nice to just run kubectl describe mydeployment
in both, and get the relevant results. Well, with this approach you can’t, because there can only be a single current-context
in your kubeconfig. So, if staging
is your current-context
you’d need to add --context production
to the commands for that context. This is also true when you’re using kubectx
to switch contexts. If you run kubectx staging
in your left terminal, it changes current-context
for the kubeconfig file, so your right terminal will also be on staging now.
The second downside is that it’s possible to run kubectx production
and forget about it. So the next time you run kubectl apply -f myhax.yaml
, it’ll go to production and not Docker Desktop… whoops! You can somewhat prevent this error by installing something like kube-ps1 which will show the current context in your prompt:
(⎈ |docker-desktop:default) [benny:~] $
Finally there’s the issue of ‘YAML merging’. Increasingly, managed Kubernetes offerings allow you to download your kubeconfig from a nice UI. But if you actually want to have a single kubeconfig, you need to merge that downloaded YAML file into your kubeconfig file. It’s not impossible, but as your YAML file gets bigger, chances of breaking things increase as well. You won’t be the first or the last to end up with a completely broken kubeconfig because they forgot an -
or :
, or because a config block was misaligned by a single space. Good times!
Approach 2: multiple kubeconfigs
A logical next step is to divide your list of contexts over multiple kubeconfig files. Initially you may be tempted to create different kubeconfig files for different projects, like ‘work’, ‘clientA’, or ‘sideprojectB’, and each of those kubeconfig files will then hold the contexts relevant to that project. To switch to a different kubeconfig, you can either add --kubeconfig <path to kubeconfig file>
to a kubectl
command, or set the KUBECONFIG
environment variable. Within a kubeconfig file, you can still use --context
or kubectx
as you would. However, the only advantage to this approach is that the list of contexts gets shorter. The other disadvantages still remain.
Approach 3: multiple kubeconfigs, each with a single context
This third approach is how I’ve currently set up my workflow. Every context has its own kubeconfig file. It addressess all downsides I’ve mentioned earlier:
- Using different contexts at the same time: simply set the
KUBECONFIG
environment variable to different kubeconfigs in your different terminals - Accidentally applying something to the wrong cluster: my
~/.kube/config
now only holds Docker Desktop, so if I accidentally apply something, I’m applying it to my local cluster which is completely fine - YAML merging: no longer an issue
Downsides?
Obviously, there are some downsides here. Otherwise everyone would be doing this, right? The biggest downside is that you now have to deal with possibly a lot of kubeconfig files. Eventually you’ll grow tired of running export KUBECONFIG=<now where did I put that file again?>
. Fortunately we can fix that.
I’ve been using 2 approaches to make switching kubeconfigs easy:
Direnv for kubeconfigs
By adding direnv to your shell you can simply drop .envrc
files wherever you like to have things set/unset in your shell. So, if I want to use a certain Kubernetes context for a certain software project, I could add the .envrc
to the git repo of that software project. Then, whenever I enter that directory, I’m automatically using the right context.
$ cd ~/git/myproject
$ cat .envrc
export KUBECONFIG=~/.env/config/myproject/kubeconfig/staging
Also, when I leave the directory ~/git/myproject
, direnv will revert $KUBECONFIG
to what it was previously (or unset it if it wasn’t set to begin with). In practice, this is how that works:
## current-context: Docker Desktop, since that is what's in ~/.kube/config
$ cd ~/git/myproject
## current-context: myproject/staging, since I set KUBECONFIG using direnv
$ cd ../
## current-context: Docker Desktop, since KUBECONFIG got unset by direnv
## after leaving the directory
$ export KUBECONFIG=~/.env/config/personal/kubeconfig/gke
## current-context: manually set to my personal GKE cluster
$ cd ~/git/myproject
## current-context: myproject/staging because of direnv
$ cd ~
## current-context: back to my personal GKE since that was what was in
## KUBECONFIG previously
$ unset KUBECONFIG
## current-context: back to Docker Desktop, since KUBECONFIG is unset
Shell Functions for Kube config
While I mostly enjoy the automation of direnv, I’m currently at a project where the switching between clusters isn’t really tied to specific Git repos or directories. I need to explicitly switch, but I still dislike long export
commands. So I’ve been extending my shell library for Situation-specific shell config for config files. You could already see a little sneak peek of that in the kubeconfig paths in the previous section.
The shell functions I’ve added assume that kubeconfigs live in my ~/.env/
tree, and are namespaced under ~/.env/config/<project>/kubeconfig
. The primary function is ks
which stands for kubeconfig-select
(but I like short commands). You can find the exact function below:
# Select KUBECONFIG file -- allows to use multiple clusters in different terminals at the same time
ks() {
case $# in
1)
local config_namespace=${ENVTOOLS_CONFIG_NAMESPACE?ERROR: Cannot determine Config Namespace}
local kubeconfig_id=${1}
;;
2)
local config_namespace=${1}
local kubeconfig_id=${2}
;;
*)
echo "USAGE: $0 <config namespace> <Kubeconfig ID>"
return 1
;;
esac
local config_dir="${HOME}/.env/config/${config_namespace}"
if [[ ! -d ${config_dir} ]]; then
echo "ERROR: Config Namespace does not exist"
return 1
fi
local kubeconfig="${config_dir}/kubeconfig/${kubeconfig_id}"
if [[ ! -r ${kubeconfig} ]]; then
echo "ERROR: Kubeconfig ${kubeconfig_id} not found"
return 1
fi
export KUBECONFIG=${kubeconfig}
kubeon
}
I can use ks
in two different ways: I can either specify the ‘config namespace’ on the command line, or set an environment variable (this is where direnv comes in again). For my current assignment I’ve basically dropped an .envrc
in ~/git/mycustomer
that sets ENVTOOLS_CONFIG_NAMESPACE
to mycustomer
, so whenever I’m in that directory tree, I can quickly specify a Kubernetes context to use:
$ cd ~/git/mycustomer
$ ks staging-aws
Boom! That’s it. The kubeon
on the final line of the ks
function also enables kube-ps1
so I can immediately see which context is active.
Conclusion
In my opinion, the cleanest and safest way to work with different Kubernetes clusters is to use multiple kubeconfig files, where every kubeconfig only holds a single context. To make it usable, add direnv or a shell function like ks
and make sure to organize those kubeconfig files in a sensible way. It ensures that you have to be specific about which cluster to interact with, with your local one as a ‘safe default’, but minimizes hassle involved with being so explicit.