Overview
#I am using k9s for my day-to-day work with Kubernetes at home and at work.
k9s is a very useful tool to administer Kubernetes clusters and their components.
Sometimes I need to debug a deployment on one of my clusters, because something went wrong, or a pod does not start, or I need to check
if the pod has actual network access to an external endpoint (like a website). Not only I, but also all my colleagues working with Kubernetes,
have some kind of small deployment which they deploy into the Kubernetes namespace to debug the problem.
But this is not all, because of the complexity of Kubernetes and its storage provisioning, environment variable handling, configmaps and
secrets. It is a real burden to write a template which mounts the same volumes, environment variables, configmaps and secrets.
I was searching for a solution. And there are k9s plugins that help.
Plugin configuration
#Usually on Unix-like operating systems, you find the k9s configuration files at ~/.config/k9s or via k9s info.
My configuration files are in the mentioned directory. k9s expects the plugins directory inside its configuration directory like
~/.config/k9s/plugins/. They are in YAML format.
volume-debug-pod.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| plugins:
# volume-debug-pod — spawn a privileged debug pod for the selected container
#
# Shortcut : Shift-Z (containers view)
# Scope : containers
#
# Prompts for a container image at runtime (default: alpine:latest), then
# creates a temporary debug pod on the same node as the target pod with:
#
# /debug/pvc/<original-mountPath> PVC volumes (read/write)
# /debug/configmaps/<original-mountPath> ConfigMap volumes
# /debug/secrets/<original-mountPath> Secret volumes
#
# All env vars (plain env + envFrom) from the target container are exported.
# The debug pod runs as root (uid 0) with full privileges.
# The pod is force-deleted automatically when the shell exits.
#
# Delegates to: ~/.config/k9s/plugins/volume-debug-pod.sh
# Requirements: kubectl, jq
volume-debug-pod:
shortCut: Shift-Z
description: Debug pod w/ volumes (image prompt)
dangerous: true
scopes:
- containers
command: bash
background: false
confirm: true
args:
- -c
- "$HOME/.config/k9s/plugins/volume-debug-pod.sh $NAMESPACE $CONTEXT $POD $NAME"
|
I will go through each of the above properties.
| Property | Description |
|---|
| plugins | It is the main entrypoint, each plugin file has to start with this property |
| plugins.string | An arbitrary name for this plugin, must be unique across all plugins |
| plugins.string.shortCut | Shortcut inside a k9s session to execute this plugin’s command + args |
| plugins.string.description | The description of this plugin, shown in the k9s help page |
| plugins.string.dangerous | Boolean flag, disables the plugin if, k9s is in ReadOnly mode |
| plugins.string.scopes | k9s scopes in which this plugin should be available and executable. Depends on what the plugin does |
| plugins.string.background | Starts the plugin command in the background |
| plugins.string.confirm | Asks for execute permission |
| plugins.string.command | The actual command like bash or stern which gets executed |
| plugins.string.args | Command-line arguments passed to the command |
There are some more options covered in the official README.md.
My First plugin
#My first plugin was inspired by two already existing plugins in debug-container.yaml
and pvc-debug-container, but they both lack some functionality
I need for my day-to-day debug sessions, mounting all PVCs, ConfigMaps, Secrets and all environment variables. Therefore I asked my AI
“Assistant” Copilot to help me create a script which extracts all those configurations from the current container.
What I wanted:
- Mount PersistentVolumes to
/debug/pvc/, read-only - Mount ConfigMaps to
/debug/configmaps - Mount Secrets to
/debug/secrets - Export the same environment variables as the current container
- Get a default
alpine:latest but allow overriding it
I just prompted the following shell script and it does what I need.
volume-debug-pod.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
| #!/usr/bin/env bash
# volume-debug-pod.sh — k9s plugin: spawn a privileged debug pod for a running container
# Run with --help or no arguments for usage information.
set -e
usage() {
cat <<EOF
USAGE
volume-debug-pod.sh <namespace> <context> <pod> <container>
ARGUMENTS
namespace Kubernetes namespace of the target pod
context kubectl context to use
pod Name of the target pod
container Name of the target container within the pod
DESCRIPTION
Spawns a temporary privileged debug pod on the same node as the target pod.
Prompts interactively for a container image (default: alpine:latest).
Volumes from the target container are mounted under:
/debug/pvc/<original-mountPath> PVC volumes (read/write)
/debug/configmaps/<original-mountPath> ConfigMap volumes
/debug/secrets/<original-mountPath> Secret volumes
All env vars (plain env + envFrom) from the target container are exported
into the debug pod. The pod runs as root (uid 0) with full privileges.
The debug pod is force-deleted automatically when the shell exits.
REQUIREMENTS
kubectl, jq
NOTES
- Debug pod is named dbg-<pod-name> (truncated to 53 chars total).
- The original pod is NOT modified — no data corruption risk.
- Process namespace is NOT shared (separate pod). Use Shift-D
(debug-container plugin) for ephemeral container injection with
shared process namespace.
- ReadWriteOnce PVCs: mounting from a second pod may be blocked by
the storage driver if the volume is exclusively bound to a node.
EXAMPLES
volume-debug-pod.sh default my-context my-pod my-container
volume-debug-pod.sh production prod-ctx web-6d4f7b8c9-xkqpz web
EOF
}
if [[ $# -eq 0 || "$1" == "--help" || "$1" == "-h" ]]; then
usage
exit 0
fi
_NS="$1"
_CTX="$2"
_POD="$3"
_CONTAINER="$4"
_DEBUG_POD="dbg-${_POD:0:40}"
read -r -p "Image [alpine:latest]: " _IMAGE
_IMAGE="${_IMAGE:-alpine:latest}"
echo "Fetching pod spec for ${_POD} / ${_CONTAINER} ..."
_POD_JSON=$(kubectl --context "${_CTX}" -n "${_NS}" get pod "${_POD}" -o json)
_DEBUG_SPEC=$(echo "${_POD_JSON}" | jq -c \
--arg podName "${_DEBUG_POD}" \
--arg ns "${_NS}" \
--arg container "${_CONTAINER}" \
--arg image "${_IMAGE}" \
'
. as $pod |
# Volumes by type
[.spec.volumes[]? | select(.persistentVolumeClaim != null)] as $pvcVols |
($pvcVols | map(.name)) as $pvcNames |
[.spec.volumes[]? | select(.configMap != null)] as $cmVols |
($cmVols | map(.name)) as $cmNames |
[.spec.volumes[]? | select(.secret != null)] as $secretVols |
($secretVols | map(.name)) as $secretNames |
# Target container spec
(.spec.containers[] | select(.name == $container)) as $ctr |
# VolumeMounts: PVCs -> /debug/pvc/<original-path>
[$ctr.volumeMounts[]? | select(.name as $n | $pvcNames | contains([$n]))
| {name: .name, mountPath: ("/debug/pvc" + .mountPath)}
] as $pvcMounts |
# VolumeMounts: configMaps -> /debug/configmaps/<original-path>
[$ctr.volumeMounts[]? | select(.name as $n | $cmNames | contains([$n]))
| {name: .name, mountPath: ("/debug/configmaps" + .mountPath)}
] as $cmMounts |
# VolumeMounts: secrets -> /debug/secrets/<original-path>
[$ctr.volumeMounts[]? | select(.name as $n | $secretNames | contains([$n]))
| {name: .name, mountPath: ("/debug/secrets" + .mountPath)}
] as $secretMounts |
{
apiVersion: "v1",
kind: "Pod",
metadata: {name: $podName, namespace: $ns},
spec: {
restartPolicy: "Never",
nodeName: $pod.spec.nodeName,
containers: [{
name: "debug",
image: $image,
command: ["/bin/sh"],
stdin: true,
tty: true,
securityContext: {
runAsUser: 0,
privileged: true,
allowPrivilegeEscalation: true
},
env: ($ctr.env // []),
envFrom: ($ctr.envFrom // []),
volumeMounts: ($pvcMounts + $cmMounts + $secretMounts)
}],
volumes: (
($pvcVols + $cmVols + $secretVols)
| map(
if .persistentVolumeClaim != null then
{name: .name, persistentVolumeClaim: {claimName: .persistentVolumeClaim.claimName}}
elif .configMap != null then
{name: .name, configMap: .configMap}
else
{name: .name, secret: .secret}
end
)
)
}
}
')
echo "Creating debug pod ${_DEBUG_POD} ..."
echo "${_DEBUG_SPEC}" | kubectl --context "${_CTX}" -n "${_NS}" apply -f - >/dev/null
cleanup() {
echo "Removing debug pod ${_DEBUG_POD} ..."
kubectl --context "${_CTX}" -n "${_NS}" delete pod "${_DEBUG_POD}" --grace-period=0 --force >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "Waiting for debug pod to be ready (timeout 60s) ..."
kubectl --context "${_CTX}" -n "${_NS}" wait --for=condition=Ready pod/"${_DEBUG_POD}" --timeout=60s
echo ""
echo "Volumes mounted under /debug/{pvc,configmaps,secrets}"
echo "
|
For more plugins check out the official k9s plugins or my own plugins at dotfiles
What I Learned
#I didn’t know anything about k9s’s plugin system. But now, every time I need something in particular e.g. multiple logs or use a filter tool for log output with stern. I know how to integrate it in a plugin.
Useful links
#