Skip to main content
  1. Blog/

K9s Plugins

·1450 words·7 mins· loading ·

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.

PropertyDescription
pluginsIt is the main entrypoint, each plugin file has to start with this property
plugins.stringAn arbitrary name for this plugin, must be unique across all plugins
plugins.string.shortCutShortcut inside a k9s session to execute this plugin’s command + args
plugins.string.descriptionThe description of this plugin, shown in the k9s help page
plugins.string.dangerousBoolean flag, disables the plugin if, k9s is in ReadOnly mode
plugins.string.scopesk9s scopes in which this plugin should be available and executable. Depends on what the plugin does
plugins.string.backgroundStarts the plugin command in the background
plugins.string.confirmAsks for execute permission
plugins.string.commandThe actual command like bash or stern which gets executed
plugins.string.argsCommand-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#

Bitfoo
Author
Bitfoo
I wrangle Kubernetes clusters and automate infrastructure by day.
At night, I run self-hosted services to keep control over my data.
I write about real DevOps challenges and the tools I actually use.
Sometimes things break. That’s when I learn the most.