When Kubernetes Lies: Seeing the Truth Inside AKS with Inspektor Gadget

Cloud platforms are built on abstractions.
We tell developers that services talk to services. We tell security teams that network policies enforce isolation. We tell architects that identity is the new perimeter and that zero trust ensures nothing moves unless explicitly allowed. In Azure Kubernetes Service, these abstractions become even more powerful. Managed control planes, private clusters, Cilium networking, workload identity, Azure Firewall, private endpoints, and policy enforcement create the feeling of a highly controlled, highly predictable platform.
And most of the time, that feeling is justified.
Until the day it isn’t.
Beneath every abstraction, whether it’s a YAML file, Azure policy, or other layer, there’s always a Linux kernel executing system calls. Containers function as processes; pods are simply cgroups; and network policies boil down to packet-filtering decisions. When issues arise unexpectedly in production, it’s the kernel that holds the definitive truth.
The problem is that most cloud tooling never shows you that truth.
Metrics reveal symptoms, logs provide interpretations, network tools display flows between pods, and security tools show alerts. However, when it comes to answering the crucial question, what actually happened?, these layers of abstraction often become obstacles rather than helpful solutions. This is where kernel-level observability changes how you operate Azure Kubernetes Service.
This is where Inspektor Gadget becomes one of the most valuable tools you can add to an AKS platform.
The gap between architecture and reality
Modern AKS environments consist of layered controls making them complex systems. A single production cluster might incorporate Azure CNI or overlay networking, Cilium or kube-proxy, private DNS zones, outbound routing via NAT Gateway or Azure Firewall, workload identity for Azure resources, admission control policies, and various observability pipelines feeding into Azure Monitor and Log Analytics.

From a design perspective, this aligns perfectly with our goals. We achieve defense-in-depth, least privilege, segmentation, auditability, and centralized monitoring. The architecture diagrams are clear, policies appear to be enforced, and dashboards indicate a healthy system.
However, runtime behavior is distinct from architecture; it refers to what processes actually perform.
When a compromised container opens a shell and begins probing internal services, this activity might not be clearly visible in application logs. Similarly, if a third-party library makes silent calls to external endpoints, metrics alone may not reveal the reason. Additionally, a misconfiguration that causes a workload to resolve public DNS instead of private DNS can introduce issues several layers removed from the initial cause.
At larger scales, these blind spots become more significant. In regulated settings, their importance grows even further. Regulations such as NIS2, DORA, and GDPR now require organizations to show operational control, not merely their intended configuration. It’s no longer sufficient to understand what your cluster is designed to do; you must also provide proof of its actual behavior.
That evidence lives in the kernel.
What Inspektor Gadget really is
Inspektor Gadget is a sandbox project under the Cloud Native Computing Foundation that aims to introduce kernel-level observability into Kubernetes. The official project website can be found at: https://www.inspektor-gadget.io/
The source code and releases are maintained on GitHub: https://github.com/inspektor-gadget/inspektor-gadget
Inspektor Gadget leverages eBPF, a Linux technology that enables small programs to run safely within the kernel. These programs are linked to specific kernel events, such as process execution, network connections, DNS resolution, file access, or system calls. Since the observation occurs directly within the kernel, visibility is precise and comprehensive, unaffected by layers above.
What sets Inspektor Gadget apart from standard eBPF tools is its Kubernetes awareness. The kernel-collected events are enhanced with Kubernetes-specific details, including namespace, pod name, container name, and labels. This allows you to identify which workload created processes and sockets, rather than just seeing anonymous data.
On AKS, this is especially important. The best practice in Azure is to avoid allowing SSH access to nodes, and changing the host is not recommended. Inspektor Gadget runs as a privileged DaemonSet, functioning within the supported Kubernetes security framework while still exposing kernel-level events.
This provides an uncommon level of insight into cloud environments: direct visibility into container activity. To install Inspektor Gadget on Azure Kubernetes Service, you need a Linux-based AKS cluster and the necessary permissions to deploy resources and run cluster-wide tools. Once your local setup is connected to the cluster, install Inspektor Gadget using Helm.
Example script for deploying Azure Kubernetes with Azure CNI Overlay and Cilium:
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# AKS + Azure CNI Overlay + Cilium dataplane + ACNS (AKS-managed Hubble Relay)
# + Azure Linux node OS
#
# References:
# - Create with ACNS + Cilium dataplane: https://learn.microsoft.com/azure/aks/how-to-configure-container-network-logs
# - ACNS overview: https://learn.microsoft.com/azure/aks/advanced-container-networking-services-overview
# - Azure Linux 3 guidance: https://learn.microsoft.com/azure/azure-linux/how-to-enable-azure-linux-3
# -----------------------------------------------------------------------------
# ====== EDIT THESE ======
LOCATION="${LOCATION:-swedencentral}" # Choose an AKS-supported region (e.g. swedensouth, westeurope, northeurope)
RESOURCE_GROUP="${RESOURCE_GROUP:-rg-aks-acns-cilium-se}"
CLUSTER_NAME="${CLUSTER_NAME:-aks-acns-cilium-se}"
NODE_COUNT="${NODE_COUNT:-2}"
NODE_VM_SIZE="${NODE_VM_SIZE:-Standard_D4s_v5}"
POD_CIDR="${POD_CIDR:-192.168.0.0/16}"
DISABLE_SSH="${DISABLE_SSH:-true}" # Set to 'true' to disable SSH access to nodes
SKIP_KUBECONFIG="${SKIP_KUBECONFIG:-false}" # Set to 'true' to skip downloading kubeconfig
KUBECONFIG_PATH="${KUBECONFIG_PATH:-${HOME}/.kube/config}" # Path to save kubeconfig
# Kubernetes version:
# ACNS+Cilium features are supported on newer versions (docs show 1.33+ examples).
# Set to an available version in your region if you want to pin.
K8S_VERSION="${K8S_VERSION:-}"
# Azure Linux:
# --os-sku AzureLinux will default to Azure Linux 3.0 on sufficiently new AKS versions.
# If you MUST pin AzureLinux3 explicitly (without changing k8s version), check your CLI supports it.
OS_SKU="${OS_SKU:-AzureLinux}"
# =======================
require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }; }
require az
require kubectl
require curl
require tar
require sha256sum
echo "==> Azure login check"
az account show >/dev/null 2>&1 || az login >/dev/null
echo "==> Preflight: validate AKS availability in ${LOCATION}"
if ! az aks get-versions --location "${LOCATION}" -o none 2>/dev/null; then
echo "AKS is not available or the location is invalid: ${LOCATION}" >&2
echo "Try another region, e.g. swedensouth, or check your subscription permissions." >&2
exit 1
fi
echo "==> Create resource group: ${RESOURCE_GROUP} (${LOCATION})"
az group create --name "${RESOURCE_GROUP}" --location "${LOCATION}" -o none
echo "==> Create AKS cluster (Azure CNI overlay + Cilium dataplane + ACNS + Azure Linux)"
AKS_CREATE_ARGS=(
--name "${CLUSTER_NAME}"
--resource-group "${RESOURCE_GROUP}"
--location "${LOCATION}"
--node-count "${NODE_COUNT}"
--node-vm-size "${NODE_VM_SIZE}"
--network-plugin azure
--network-plugin-mode overlay
--pod-cidr "${POD_CIDR}"
--network-dataplane cilium
--enable-acns
--os-sku "${OS_SKU}"
)
if [[ "${DISABLE_SSH}" != "true" ]]; then
AKS_CREATE_ARGS+=( --generate-ssh-keys )
fi
if [[ -n "${K8S_VERSION}" ]]; then
AKS_CREATE_ARGS+=( --kubernetes-version "${K8S_VERSION}" )
fi
az aks create "${AKS_CREATE_ARGS[@]}" -o none
echo "==> Get kubeconfig"
if [[ "${SKIP_KUBECONFIG}" == "true" ]]; then
echo " (Skipping kubeconfig download)"
else
mkdir -p "$(dirname "${KUBECONFIG_PATH}")"
az aks get-credentials \
--resource-group "${RESOURCE_GROUP}" \
--name "${CLUSTER_NAME}" \
--file "${KUBECONFIG_PATH}" \
--overwrite-existing -o none
echo " Kubeconfig saved to: ${KUBECONFIG_PATH}"
export KUBECONFIG="${KUBECONFIG_PATH}"
fi
echo "==> Verify nodes (ensure OS is Azure Linux)"
kubectl get nodes -o wide
echo "==> Verify Cilium pods exist (AKS-managed Cilium runs in kube-system)"
kubectl -n kube-system get pods | grep -E "^cilium-" >/dev/null
echo "==> Verify ACNS-managed Hubble Relay is running"
# Microsoft docs use label k8s-app=hubble-relay
kubectl get pods -n kube-system -l k8s-app=hubble-relay -o wide
echo "==> Install Hubble CLI (as per Microsoft docs)"
# Pin the version shown in docs; update if your cluster components require a newer one.
HUBBLE_VERSION="${HUBBLE_VERSION:-v1.16.3}"
HUBBLE_ARCH="amd64"
if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]]; then HUBBLE_ARCH="arm64"; fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT
(
cd "${tmpdir}"
curl -L --fail --remote-name-all \
"https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-${HUBBLE_ARCH}.tar.gz" \
"https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-${HUBBLE_ARCH}.tar.gz.sha256sum"
sha256sum --check "hubble-linux-${HUBBLE_ARCH}.tar.gz.sha256sum"
# Install to /usr/local/bin if writable, else ~/.local/bin
install_dir="/usr/local/bin"
if [[ ! -w "${install_dir}" ]]; then
install_dir="${HOME}/.local/bin"
mkdir -p "${install_dir}"
export PATH="${install_dir}:${PATH}"
fi
tar xzvfC "hubble-linux-${HUBBLE_ARCH}.tar.gz" "${install_dir}"
)
echo "==> Port-forward Hubble Relay (leave this running in a separate terminal when observing flows)"
echo " Command:"
echo " kubectl port-forward -n kube-system svc/hubble-relay --address 127.0.0.1 4245:443"
echo
echo "==> After port-forward is running, you can observe flows with:"
echo " hubble status"
echo " hubble observe --follow"
echo
# -----------------------------------------------------------------------------
# Optional: Deploy Hubble UI (Microsoft manifest pattern) + port-forward
# This expects the secret 'hubble-relay-client-certs' to exist (ACNS-managed Hubble creates it).
# -----------------------------------------------------------------------------
echo "==> Deploy Hubble UI (optional but recommended)"
cat <<'YAML' | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: hubble-ui
namespace: kube-system
labels:
app.kubernetes.io/part-of: retina
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hubble-ui
labels:
app.kubernetes.io/part-of: retina
rules:
- apiGroups: [""]
resources: ["namespaces", "pods", "services"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: hubble-ui
labels:
app.kubernetes.io/part-of: retina
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: hubble-ui
subjects:
- kind: ServiceAccount
name: hubble-ui
namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: hubble-ui-nginx
namespace: kube-system
data:
nginx.conf: |
server {
listen 8081;
server_name _;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /healthz {
add_header Content-Type text/plain;
return 200 'ok';
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hubble-ui
namespace: kube-system
labels:
k8s-app: hubble-ui
app.kubernetes.io/name: hubble-ui
app.kubernetes.io/part-of: retina
spec:
replicas: 1
selector:
matchLabels:
k8s-app: hubble-ui
template:
metadata:
labels:
k8s-app: hubble-ui
app.kubernetes.io/name: hubble-ui
app.kubernetes.io/part-of: retina
spec:
serviceAccountName: hubble-ui
automountServiceAccountToken: true
nodeSelector:
kubernetes.io/os: linux
containers:
- name: frontend
image: mcr.microsoft.com/oss/cilium/hubble-ui:v0.12.2
imagePullPolicy: Always
ports:
- name: http
containerPort: 8081
livenessProbe:
httpGet:
path: /healthz
port: 8081
readinessProbe:
httpGet:
path: /
port: 8081
volumeMounts:
- name: hubble-ui-nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.conf
- name: tmp-dir
mountPath: /tmp
- name: backend
image: mcr.microsoft.com/oss/cilium/hubble-ui-backend:v0.12.2
imagePullPolicy: Always
env:
- name: EVENTS_SERVER_PORT
value: "8090"
- name: FLOWS_API_ADDR
value: "hubble-relay:443"
- name: TLS_TO_RELAY_ENABLED
value: "true"
- name: TLS_RELAY_SERVER_NAME
value: ui.hubble-relay.cilium.io
- name: TLS_RELAY_CA_CERT_FILES
value: /var/lib/hubble-ui/certs/hubble-relay-ca.crt
- name: TLS_RELAY_CLIENT_CERT_FILE
value: /var/lib/hubble-ui/certs/client.crt
- name: TLS_RELAY_CLIENT_KEY_FILE
value: /var/lib/hubble-ui/certs/client.key
livenessProbe:
httpGet:
path: /healthz
port: 8090
readinessProbe:
httpGet:
path: /healthz
port: 8090
ports:
- name: grpc
containerPort: 8090
volumeMounts:
- name: hubble-ui-client-certs
mountPath: /var/lib/hubble-ui/certs
readOnly: true
volumes:
- name: hubble-ui-nginx-conf
configMap:
name: hubble-ui-nginx
- name: tmp-dir
emptyDir: {}
- name: hubble-ui-client-certs
projected:
defaultMode: 0400
sources:
- secret:
name: hubble-relay-client-certs
items:
- key: tls.crt
path: client.crt
- key: tls.key
path: client.key
- key: ca.crt
path: hubble-relay-ca.crt
---
apiVersion: v1
kind: Service
metadata:
name: hubble-ui
namespace: kube-system
labels:
k8s-app: hubble-ui
app.kubernetes.io/name: hubble-ui
app.kubernetes.io/part-of: retina
spec:
type: ClusterIP
selector:
k8s-app: hubble-ui
ports:
- name: http
port: 80
targetPort: 8081
YAML
echo
echo "==> Hubble UI deployed. To access it:"
echo " kubectl -n kube-system port-forward svc/hubble-ui 12000:80"
echo " then open: http://localhost:12000/"
echo
echo "DONE."
First, retrieve cluster credentials.
az aks get-credentials \
--resource-group rg-aks-acns-cilium-se \
--name aks-acns-cilium-se
Add the Helm repository and install the DaemonSet.
helm repo add inspektor-gadget https://inspektor-gadget.github.io/charts
helm repo update
helm install ig inspektor-gadget/gadget \
--namespace gadget \
--create-namespace
After a short moment, verify that the pods are running on all nodes.
kubectl get pods -n gadget
At this point, kernel probes are active across the cluster. Nothing has changed in your applications. No sidecars were injected. No code was modified. Yet the cluster is now capable of exposing runtime behavior that previously required node access.
To query this data, install the kubectl plugin via Krew.
kubectl krew install gadget
You can confirm the installation with:
kubectl gadget version
The cluster is now ready for kernel-level observation.
Seeing processes instead of containers
One of the simplest but most powerful demonstrations is tracing process execution.
Run:
kubectl gadget run trace_exec
Then start a test workload in another terminal.
kubectl run demo --image=busybox -- sleep 3600
kubectl exec -it demo -- sh
Inside the shell, run a simple command like ls. The trace will immediately display the process execution, including the namespace, pod, and container details. Many engineers realize what Inspektor Gadget reveals at this moment. Typically, Kubernetes hides processes behind container boundaries, but with kernel tracing, you can see exactly what runs within those boundaries. Unexpected shells, debugging tools, package installers, or custom binaries become instantly visible.
This level of clarity is especially transformative for incident response.
Understanding runtime behavior through DNS and TCP
Most production issues in distributed systems eventually involve networking. But networking problems often start with name resolution. A service might resolve a public endpoint instead of a private one. A library might call an undocumented telemetry endpoint. A compromised container might attempt to communicate with a command-and-control server.
To observe DNS activity across the cluster:
kubectl gadget run trace_dns
Similarly, TCP connections can be monitored with:
kubectl gadget run trace_tcp
These traces show which workloads resolve specific domains and the connections they make. Since the data originates from the kernel, it accurately represents real runtime behavior rather than just the declared configuration.
In AKS environments with private endpoints, outbound filtering, and strict egress controls, this visibility enables platform teams to verify that traffic flows align with the architectural design.
Where Cilium Hubble fits into the picture
Many AKS clusters already utilize Cilium and Hubble for network observability. Hubble offers valuable insights into network traffic between pods and services, including policy verdicts and, in some cases, application-layer information.
However, Hubble operates at the network layer, whereas Inspektor Gadget operates at the kernel layer.
Hubble can identify when a pod is connected to an external IP address. Inspektor Gadget determines which process opened the socket. Hubble indicates whether the traffic was allowed or denied, while Inspektor Gadget shows the binary and command responsible for generating the traffic.
In practice, the two tools work together. Hubble determines whether the network policy functioned correctly, while Inspektor Gadget evaluates the workload’s behavior.

In zero-trust architectures, both perspectives are necessary.
More information about Hubble can be found at:
https://docs.cilium.io/en/stable/gettingstarted/hubble/
A real production scenario
Consider a situation that many platform teams eventually face.
A firewall administrator observes outbound traffic from an AKS subnet to public IP addresses that are not listed in the approved dependency list. The applications seem to be functioning normally. There are no clear errors in the logs, metrics are normal, and no security alerts have been triggered.
The platform team receives the question: which workload is responsible for this traffic?
Instead of searching through logs or reviewing application code, the team begins with runtime observation.
Creating a realistic workload in the payments namespace
Start by creating the namespace
kubectl create namespace payments
Now deploy a small worker pod that continuously performs DNS lookups and outbound HTTPS calls. This simulates the behavior of a service reaching external dependencies.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: payments-worker
namespace: payments
labels:
app: payments-worker
spec:
restartPolicy: Always
containers:
- name: worker
image: wbitt/network-multitool
command: ["/bin/sh","-c"]
args:
- |
echo "Starting payments worker simulation..."
while true; do
# DNS lookups
nslookup example.com >/dev/null 2>&1 || true
nslookup kubernetes.default.svc.cluster.local >/dev/null 2>&1 || true
# Outbound HTTPS call
curl -s https://example.com >/dev/null 2>&1 || true
# Simulate periodic command execution
/bin/sh -c "date > /tmp/heartbeat"
sleep 10
done
EOF
Observing DNS activity
In one terminal, start the DNS trace.
kubectl gadget run trace_dns -n payments

Within seconds, repeated queries appear for an unfamiliar domain.
Observing outbound connections
In another terminal, observe TCP activity.
kubectl gadget run trace_tcp:latest -n payments

You will see outbound connections to public IP addresses on port 443. In a real incident, this is where platform teams start asking important questions:
Why is this workload talking to the internet?
Is this expected behavior?
Does it match the architecture?
Observing process execution
Finally, observe process activity.
kubectl gadget run trace_exec:latest -n payments

The trace reveals a shell process executing a command periodically inside a container that was supposed to run only a Java application.
At this point, the investigation moves from speculation to evidence. The workload image can be inspected, the deployment history reviewed, and containment actions taken with confidence.
This is the difference between observing symptoms and observing reality.
When finished, clean up:
kubectl delete namespace payments
Cleanup script for removing the AKS Cluster
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# Cleanup for the AKS + ACNS + Cilium + Hubble UI deployment script
#
# What it does:
# 1) Deletes the optional Hubble UI resources created in kube-system (if present)
# 2) Deletes the AKS cluster (optional; default true)
# 3) Deletes the resource group (optional; default true) ✅ recommended
# 4) Optionally removes the kubeconfig entry/context for the cluster
#
# Usage:
# chmod +x cleanup-aks-acns.sh
# ./cleanup-aks-acns.sh
#
# Optional flags:
# ./cleanup-aks-acns.sh --force
# ./cleanup-aks-acns.sh --no-wait
# DELETE_RG=false ./cleanup-aks-acns.sh
# DELETE_AKS=false ./cleanup-aks-acns.sh
# REMOVE_KUBECONFIG=true ./cleanup-aks-acns.sh
#
# Docs:
# - az group delete: https://learn.microsoft.com/cli/azure/group#az-group-delete
# - az aks delete: https://learn.microsoft.com/cli/azure/aks#az-aks-delete
# -----------------------------------------------------------------------------
# Must match your deploy defaults (override via env vars)
LOCATION="${LOCATION:-swedencentral}"
RESOURCE_GROUP="${RESOURCE_GROUP:-rg-aks-acns-cilium-se}"
CLUSTER_NAME="${CLUSTER_NAME:-aks-acns-cilium-se}"
KUBECONFIG_PATH="${KUBECONFIG_PATH:-${HOME}/.kube/config}"
# Behavior toggles
DELETE_AKS="${DELETE_AKS:-true}" # delete cluster explicitly (not needed if deleting RG, but useful if DELETE_RG=false)
DELETE_RG="${DELETE_RG:-true}" # recommended: deletes everything created by deployment
DELETE_HUBBLE_UI="${DELETE_HUBBLE_UI:-true}"
REMOVE_KUBECONFIG="${REMOVE_KUBECONFIG:-false}" # remove kubeconfig context/user/cluster entries
FORCE=false
NO_WAIT=false
for arg in "$@"; do
case "$arg" in
--force) FORCE=true ;;
--no-wait) NO_WAIT=true ;;
esac
done
require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }; }
require az
echo "========================================"
echo " Cleanup: AKS + ACNS + Cilium + Hubble UI"
echo " Location : ${LOCATION}"
echo " Resource Group: ${RESOURCE_GROUP}"
echo " Cluster : ${CLUSTER_NAME}"
echo "========================================"
echo
# Azure login
az account show >/dev/null 2>&1 || az login >/dev/null
# Confirm deletion (unless forced)
if [[ "${FORCE}" != "true" ]]; then
echo "This cleanup may delete:"
[[ "${DELETE_HUBBLE_UI}" == "true" ]] && echo " - Hubble UI resources in kube-system (Deployment/Service/ConfigMap/RBAC)"
[[ "${DELETE_AKS}" == "true" ]] && echo " - AKS cluster: ${CLUSTER_NAME} (if it exists)"
[[ "${DELETE_RG}" == "true" ]] && echo " - Resource group: ${RESOURCE_GROUP} (and ALL contained resources) [recommended]"
[[ "${REMOVE_KUBECONFIG}" == "true" ]] && echo " - kubeconfig entries for ${CLUSTER_NAME} in ${KUBECONFIG_PATH}"
echo
read -r -p "Type the resource group name to confirm: " CONFIRM
if [[ "${CONFIRM}" != "${RESOURCE_GROUP}" ]]; then
echo "Confirmation failed. Aborting."
exit 1
fi
fi
# If we plan to touch Kubernetes objects, try to get credentials (best-effort)
if [[ "${DELETE_HUBBLE_UI}" == "true" ]]; then
if command -v kubectl >/dev/null 2>&1; then
echo "==> Attempting to delete Hubble UI resources from kube-system (best-effort)"
# best-effort kubeconfig usage
export KUBECONFIG="${KUBECONFIG_PATH}"
# Try to get credentials (won't fail the script if cluster is already gone)
az aks get-credentials -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}" --file "${KUBECONFIG_PATH}" --overwrite-existing -o none 2>/dev/null || true
# Delete resources created by the deploy script (ignore if not found)
kubectl -n kube-system delete svc hubble-ui --ignore-not-found || true
kubectl -n kube-system delete deploy hubble-ui --ignore-not-found || true
kubectl -n kube-system delete cm hubble-ui-nginx --ignore-not-found || true
kubectl delete clusterrolebinding hubble-ui --ignore-not-found || true
kubectl delete clusterrole hubble-ui --ignore-not-found || true
kubectl -n kube-system delete sa hubble-ui --ignore-not-found || true
else
echo "==> kubectl not found; skipping Hubble UI Kubernetes resource cleanup."
echo " (If you delete the resource group, these will be removed anyway.)"
fi
fi
# Delete AKS cluster (optional)
if [[ "${DELETE_AKS}" == "true" && "${DELETE_RG}" != "true" ]]; then
echo "==> Deleting AKS cluster '${CLUSTER_NAME}' (resource group '${RESOURCE_GROUP}')"
if [[ "${NO_WAIT}" == "true" ]]; then
az aks delete -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}" --yes --no-wait || true
echo " AKS deletion started (no-wait)."
else
az aks delete -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}" --yes || true
echo " AKS deletion completed."
fi
else
if [[ "${DELETE_RG}" == "true" ]]; then
echo "==> Skipping explicit AKS delete because resource group deletion will remove it."
fi
fi
# Delete resource group (recommended)
if [[ "${DELETE_RG}" == "true" ]]; then
echo "==> Deleting resource group '${RESOURCE_GROUP}' (this removes ALL resources in it)"
if [[ "${NO_WAIT}" == "true" ]]; then
az group delete --name "${RESOURCE_GROUP}" --yes --no-wait
echo " Resource group deletion started (no-wait)."
else
az group delete --name "${RESOURCE_GROUP}" --yes
echo " Resource group deletion completed."
fi
fi
# Optionally remove kubeconfig entries
if [[ "${REMOVE_KUBECONFIG}" == "true" ]]; then
if command -v kubectl >/dev/null 2>&1; then
echo "==> Removing kubeconfig entries for '${CLUSTER_NAME}' from ${KUBECONFIG_PATH}"
export KUBECONFIG="${KUBECONFIG_PATH}"
# AKS usually uses contexts like "<cluster-name>" or "<cluster-name>-admin"
# We'll remove any context that contains the cluster name.
mapfile -t contexts < <(kubectl config get-contexts -o name 2>/dev/null | grep -F "${CLUSTER_NAME}" || true)
for ctx in "${contexts[@]}"; do
kubectl config delete-context "${ctx}" >/dev/null 2>&1 || true
done
# Try to remove cluster entry and user entry that commonly match the cluster name
# These names vary, so we do best-effort matching.
mapfile -t clusters < <(kubectl config get-clusters 2>/dev/null | tail -n +2 | grep -F "${CLUSTER_NAME}" || true)
for c in "${clusters[@]}"; do
kubectl config delete-cluster "${c}" >/dev/null 2>&1 || true
done
mapfile -t users < <(kubectl config get-users 2>/dev/null | tail -n +2 | grep -F "${CLUSTER_NAME}" || true)
for u in "${users[@]}"; do
kubectl config unset "users.${u}" >/dev/null 2>&1 || true
done
echo " kubeconfig cleanup complete."
else
echo "==> kubectl not found; skipping kubeconfig cleanup."
fi
fi
echo
echo "Cleanup finished."
Operational and governance considerations
Kernel-level observability is a powerful capability and must be handled with care. Access to Inspektor Gadget should be restricted using Kubernetes RBAC, and its deployment should adhere to operational procedures similar to those for other privileged diagnostic tools.
In most organizations, this responsibility typically falls to platform engineering, security operations, or incident response teams instead of general developers.
From a compliance perspective, the value is considerable. Inspektor Gadget offers proof of runtime behavior, which can aid forensic investigations, fulfill audit requirements, and support zero-trust validation.
It enables teams to show not just policy configurations but also that workloads operate within expected limits.
Conclusion: closing the gap between design and execution
Cloud architecture has grown highly complex. We develop identity-focused access systems, segmented networks, encrypted communication channels, and policy-based deployment pipelines. Azure Kubernetes Service offers managed infrastructure that ensures these designs are scalable and dependable.
However, architecture reflects intent; security, reliability, and compliance ultimately rely on execution.
Execution happens in the kernel.
Inspektor Gadget offers more than just another observability tool for your platform. It reestablishes visibility into the layer where containers transition into processes, where policies turn into packet decisions, and where abstractions fade away.
For platform teams managing AKS at scale, this alters the approach to incident investigation, validation of zero-trust assumptions, and demonstration of operational control.
When integrated with tools such as Azure Monitor, Microsoft Defender for Containers, and Cilium Hubble, kernel-level visibility completes the observability stack. Metrics reveal performance, logs clarify behavior, network flows illustrate communication, and the kernel provides the underlying reality.
Once you’ve viewed your cluster from that angle, it becomes hard to depend solely on abstractions.
Ultimately, Kubernetes may describe the system you aimed to create, but the kernel reveals the system that is actually running.