We have spent the whole module writing rutas-norte-dev in every manifest and every command without stopping to explain what that namespace actually is or what it gives us. And at the end of the previous lesson a very interesting property turned up: bookings-api's configuration points at redis-cache and bookings-postgres by their short names, without mentioning the environment, because DNS resolves them within the namespace the pod runs in. That property is what makes it possible to deploy the same manifest three times and get three independent platforms. In this lesson we set up Rutas Norte's three environments —rutas-norte-dev, rutas-norte-pre and rutas-norte-pro—, we will see what a namespace isolates and what it does not isolate at all (the part that causes the most incidents), we will tell namespaced resources apart from cluster-scoped ones, we will meet the system namespaces and why nothing is deployed to default, we will work comfortably with -n and with the context, we will resolve DNS names across namespaces, and we will finish with a very serious warning about what a kubectl delete namespace takes down with it.

Contents

  1. What a namespace is and what it isolates
  2. Namespaced and cluster-scoped resources
  3. The system namespaces and why nothing is deployed to default
  4. The three Rutas Norte environments
  5. The same manifest in several environments
  6. Working with namespaces without going mad
  7. DNS across namespaces
  8. What a namespace does NOT guarantee
  9. Deleting a namespace: the most dangerous kubectl command
  10. Namespaces versus separate clusters

  1. What a namespace is and what it isolates

A namespace is a logical partition of the cluster that groups resources. The operational definition, the one that really helps:

A namespace is, above all, a name space: inside it, every object's name must be unique; outside it, names can repeat freely.

That allows three objects called bookings-api to exist at the same time, one per environment, without colliding. And every other use follows from there.

What a namespace does provide:

Function What it means in practice
Name uniqueness bookings-api can exist in dev, pre and pro at once
Scope of queries kubectl get pods only shows those in the current namespace
Anchor point for quotas A ResourceQuota limits a namespace's total consumption (03-04)
Unit of RBAC permissions A Role grants permissions within a namespace (08-01)
Subject of network policies A NetworkPolicy can select by namespace (04-06)
Scope of short DNS names redis-cache resolves to the Service in the asking pod's namespace
Scope of selectors A Service or a ReplicaSet only sees pods in its own namespace
Bulk deletion Deleting the namespace removes everything it contains

And what it does not provide, which we will look at in detail in section 8: it does not isolate the network, it does not limit resources on its own, it does not prevent access and it does not separate the nodes.

flowchart TB
    subgraph CL["minikube cluster · rutas-norte profile"]
        subgraph DEV["namespace rutas-norte-dev"]
            D1["Deployment bookings-api"]
            D2["Service bookings-api"]
            D3["Deployment bookings-postgres"]
        end
        subgraph PRE["namespace rutas-norte-pre"]
            E1["Deployment bookings-api"]
            E2["Service bookings-api"]
            E3["Deployment bookings-postgres"]
        end
        subgraph SYS["namespace kube-system"]
            S1["CoreDNS"]
            S2["kube-proxy"]
        end
        NODES["Nodes, PersistentVolumes, StorageClasses<br/>(cluster-scoped: shared by everything)"]
    end

Note the key detail of the diagram: objects with the same name coexist in different namespaces, but the nodes are shared. A pod from rutas-norte-dev and one from rutas-norte-pre can end up on the same machine.

  1. Namespaced and cluster-scoped resources

Not every Kubernetes object lives inside a namespace. Some belong to the whole cluster, and confusing the two produces baffling errors.

The way to know for certain, without memorising anything:

kubectl api-resources --namespaced=true | head -15
NAME                    SHORTNAMES   APIVERSION    NAMESPACED   KIND
bindings                             v1            true         Binding
configmaps              cm           v1            true         ConfigMap
endpoints               ep           v1            true         Endpoints
events                  ev           v1            true         Event
limitranges             limits       v1            true         LimitRange
persistentvolumeclaims  pvc          v1            true         PersistentVolumeClaim
pods                    po           v1            true         Pod
replicationcontrollers  rc           v1            true         ReplicationController
resourcequotas          quota        v1            true         ResourceQuota
secrets                              v1            true         Secret
serviceaccounts         sa           v1            true         ServiceAccount
services                svc          v1            true         Service
deployments             deploy       apps/v1       true         Deployment
replicasets             rs           apps/v1       true         ReplicaSet
statefulsets            sts          apps/v1       true         StatefulSet
kubectl api-resources --namespaced=false
NAME                        SHORTNAMES   APIVERSION                        NAMESPACED   KIND
componentstatuses           cs           v1                                false        ComponentStatus
namespaces                  ns           v1                                false        Namespace
nodes                       no           v1                                false        Node
persistentvolumes           pv           v1                                false        PersistentVolume
mutatingwebhookconfigurations            admissionregistration.k8s.io/v1   false        MutatingWebhookConfiguration
customresourcedefinitions   crd          apiextensions.k8s.io/v1           false        CustomResourceDefinition
apiservices                              apiregistration.k8s.io/v1         false        APIService
tokenreviews                             authentication.k8s.io/v1          false        TokenReview
clusterrolebindings                      rbac.authorization.k8s.io/v1      false        ClusterRoleBinding
clusterroles                             rbac.authorization.k8s.io/v1      false        ClusterRole
priorityclasses             pc           scheduling.k8s.io/v1              false        PriorityClass
csidrivers                               storage.k8s.io/v1                 false        CSIDriver
storageclasses              sc           storage.k8s.io/v1                 false        StorageClass
volumeattachments                        storage.k8s.io/v1                 false        VolumeAttachment

The logic behind the split is coherent: anything representing physical infrastructure or global cluster configuration carries no namespace; anything representing a workload or application configuration does.

Namespaced Cluster-scoped
Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, CronJob Node
Service, Endpoints, EndpointSlice, Ingress, NetworkPolicy Namespace
ConfigMap, Secret, ServiceAccount PersistentVolume, StorageClass, CSIDriver
PersistentVolumeClaim ClusterRole, ClusterRoleBinding
Role, RoleBinding CustomResourceDefinition, PriorityClass
ResourceQuota, LimitRange, HorizontalPodAutoscaler IngressClass

Two pairs deserve attention because they are a constant source of confusion:

  • PersistentVolume (cluster-scoped) versus PersistentVolumeClaim (namespaced). The disk is a cluster resource; the claim on that disk belongs to a specific application in a namespace. Module 5.
  • Role/RoleBinding (namespaced) versus ClusterRole/ClusterRoleBinding (cluster-scoped). The former grant permissions within a namespace; the latter, across the whole cluster. Module 8.

A practical mistake that follows from this:

kubectl get nodes -n rutas-norte-dev
NAME          STATUS   ROLES           AGE   VERSION
rutas-norte   Ready    control-plane   3d    v1.30.0

The -n has been silently ignored. Nodes have no namespace, so the flag filters nothing. It is not an error, but it can make you believe you are seeing something filtered when you are not.

  1. The system namespaces and why nothing is deployed to default

Every cluster is born with four namespaces:

kubectl get namespaces
NAME              STATUS   AGE
default           Active   3d
kube-node-lease   Active   3d
kube-public       Active   3d
kube-system       Active   3d
rutas-norte-dev   Active   2d
Namespace Contents Should you touch it?
default Empty at first. It is where objects go if you name no namespace Do not deploy here
kube-system Kubernetes' own components: CoreDNS, kube-proxy, CSI controllers, addons Never deploy here; look, do not touch
kube-public Readable without authenticating; it holds a ConfigMap with public cluster data Almost never used
kube-node-lease One Lease object per node, which the kubelet renews as a heartbeat Never

Take a look at kube-system to see the cluster from the inside:

kubectl get pods -n kube-system
NAME                                  READY   STATUS    RESTARTS   AGE
coredns-668d6bf9bc-x8k2m              1/1     Running   0          3d
etcd-rutas-norte                      1/1     Running   0          3d
kube-apiserver-rutas-norte            1/1     Running   0          3d
kube-controller-manager-rutas-norte   1/1     Running   0          3d
kube-proxy-7wfnz                      1/1     Running   0          3d
kube-scheduler-rutas-norte            1/1     Running   0          3d
metrics-server-6d94bc8694-p4rzt       1/1     Running   0          3d
storage-provisioner                   1/1     Running   0          3d

There they are, running as pods, all the components you studied in Kubernetes Architecture. Your minikube's control plane is visible and tangible.

kube-node-lease explains something you may have wondered about: how the control plane knows whether a node is still alive.

kubectl get leases -n kube-node-lease
NAME          HOLDER        AGE
rutas-norte   rutas-norte   3d

The kubelet renews that object every few seconds. If it stops doing so, the node is marked NotReady.

Why nothing is deployed to default

It is the namespace Kubernetes uses when you say nothing, and that is precisely why it is a bad place to work:

  1. It is the cluster's dumping ground. Everything anybody applies without -n ends up there. Within a few months it is a junk drawer where nobody knows what belongs to whom.
  2. It cannot be deleted. The system namespaces are indestructible, so you cannot clear it out in one go the way you would with rutas-norte-dev.
  3. Accidents are easy. A kubectl delete deploy --all without -n, run in the belief you were somewhere else, takes down whatever is in default.
  4. It makes isolation impossible. If everything is in default, you cannot apply per-team quotas, per-environment RBAC or differentiated network policies.
  5. It prevents reusing names. With everything in one namespace, the development and production bookings-api cannot coexist.

The project rule is explicit:

At Rutas Norte, default is forbidden. Every object lives in a named namespace, declared in its own manifest.

  1. The three Rutas Norte environments

The time has come to create the full structure. You already know rutas-norte-dev from module 1; we add preproduction and production, declaratively and in a single file.

In YAML, --- separates several documents within the same file. It is the usual way of grouping related objects:

# k8s/base/namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-dev
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
---
apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-pre
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: pre
---
apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: pro
  annotations:
    rutasnorte.example/owner: [email protected]
    rutasnorte.example/notice: "Production environment. Changes through the pipeline only."
kubectl apply -f k8s/base/namespaces.yaml
kubectl get namespaces -l app.kubernetes.io/part-of=rutas-norte
namespace/rutas-norte-dev configured
namespace/rutas-norte-pre created
namespace/rutas-norte-pro created

NAME              STATUS   AGE
rutas-norte-dev   Active   2d
rutas-norte-pre   Active   4s
rutas-norte-pro   Active   4s

Note two deliberate decisions:

  • The namespaces carry labels. This is not decorative: in module 4, NetworkPolicies will select namespaces by the environment label, so a policy will be able to say "I only accept traffic from pods in the same environment".
  • Production carries informative annotations. Anyone running kubectl describe ns rutas-norte-pro will know who to notify and what rules apply.

The repository structure we already defined fits naturally:

k8s/
├── base/                  # manifests common to the three environments
│   ├── namespaces.yaml
│   ├── web-store-deployment.yaml
│   ├── web-store-service.yaml
│   ├── bookings-api-deployment.yaml
│   └── ...
└── environments/
    ├── dev/               # differences for the development environment
    ├── pre/
    └── pro/

How base and environments are combined without duplicating YAML is the job of Kustomize or Helm, in 10-04 and 10-03. In this module we will do it as simply as possible.

  1. The same manifest in several environments

Here is the property that makes namespaces valuable. We are going to deploy web-store and its Service to preproduction using exactly the same files as in development.

Our manifests carry namespace: rutas-norte-dev written inside them, so there are two ways of taking them to another environment.

Option A: remove namespace from the manifest and decide it at apply time.

# k8s/base/web-store-deployment.yaml (fragment, no namespace)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-store
  # no namespace field: whoever applies it decides
  labels:
    app: web-store
    app.kubernetes.io/part-of: rutas-norte
kubectl apply -f k8s/base/web-store-deployment.yaml -n rutas-norte-pre
kubectl apply -f k8s/base/web-store-service.yaml -n rutas-norte-pre

Option B: keep namespace in the manifest and substitute it at deploy time. This is what Kustomize or Helm will do for you; by hand it would look like this:

sed 's/rutas-norte-dev/rutas-norte-pre/g' k8s/base/web-store-deployment.yaml | kubectl apply -f -
sed 's/rutas-norte-dev/rutas-norte-pre/g' k8s/base/web-store-service.yaml | kubectl apply -f -

There is an important rule worth knowing: if a manifest declares metadata.namespace and you also pass -n with a different value, kubectl errors out instead of choosing on its own.

kubectl apply -f k8s/base/web-store-deployment.yaml -n rutas-norte-pre
error: the namespace from the provided object "rutas-norte-dev" does not match
the namespace "rutas-norte-pre". You must pass '--namespace=rutas-norte-dev' to perform this operation.

It is desirable behaviour: it prevents deploying to production by accident.

Let's do it with option B, which respects our current manifests:

for f in web-store-deployment web-store-service bookings-api-deployment bookings-api-service redis-cache-deployment redis-cache-service; do
  sed 's/rutas-norte-dev/rutas-norte-pre/g' k8s/base/$f.yaml | kubectl apply -f -
done
kubectl get deploy,svc -n rutas-norte-pre
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/bookings-api  2/2     2            2           25s
deployment.apps/redis-cache   1/1     1            1           24s
deployment.apps/web-store     3/3     3            3           26s

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/bookings-api   ClusterIP   10.96.229.14    <none>        3000/TCP   25s
service/redis-cache    ClusterIP   10.96.88.107    <none>        6379/TCP   24s
service/web-store      ClusterIP   10.96.44.71     <none>        80/TCP     26s

Now look at the cluster's full situation:

kubectl get pods -A -l app.kubernetes.io/part-of=rutas-norte
NAMESPACE         NAME                                     READY   STATUS    RESTARTS   AGE
rutas-norte-dev   bookings-api-7f1d3e942-b8mrs             1/1     Running   0          48m
rutas-norte-dev   bookings-api-7f1d3e942-n4jvt             1/1     Running   0          48m
rutas-norte-dev   bookings-postgres-59d7c4b8f-k3xqp        1/1     Running   0          40m
rutas-norte-dev   redis-cache-6c8f9d745-w2mtb              1/1     Running   0          52m
rutas-norte-dev   web-store-6f9c4b8d7-42kxr                1/1     Running   0          55m
rutas-norte-dev   web-store-6f9c4b8d7-8vnwq                1/1     Running   0          55m
rutas-norte-dev   web-store-6f9c4b8d7-t9mzd                1/1     Running   0          55m
rutas-norte-pre   bookings-api-7f1d3e942-c9plk             1/1     Running   0          1m
rutas-norte-pre   bookings-api-7f1d3e942-r5vwj             1/1     Running   0          1m
rutas-norte-pre   redis-cache-6c8f9d745-h8nqz              1/1     Running   0          1m
rutas-norte-pre   web-store-6f9c4b8d7-d3jbx                1/1     Running   0          1m
rutas-norte-pre   web-store-6f9c4b8d7-m7kwr                1/1     Running   0          1m
rutas-norte-pre   web-store-6f9c4b8d7-p2fst                1/1     Running   0          1m

Two complete Rutas Norte platforms, with the same Deployment and Service names, coexisting without the slightest collision. That is the namespace's core value.

And a check that illustrates the name isolation:

kubectl get svc bookings-api -n rutas-norte-dev -o jsonpath='{.spec.clusterIP}{"\n"}'
kubectl get svc bookings-api -n rutas-norte-pre -o jsonpath='{.spec.clusterIP}{"\n"}'
10.96.184.22
10.96.229.14

Same name, two different objects, two different virtual IPs.

  1. Working with namespaces without going mad

Typing -n rutas-norte-dev a hundred times a day is tedious and, worse, forgetting it is the fast track to an accident.

The -n and -A flags

kubectl get pods -n rutas-norte-pre          # one specific namespace
kubectl get pods --all-namespaces            # all of them
kubectl get pods -A                          # shorthand for the above

-A is indispensable when you are looking for something and cannot remember where it is:

kubectl get deploy -A -l app=bookings-api
NAMESPACE         NAME           READY   UP-TO-DATE   AVAILABLE   AGE
rutas-norte-dev   bookings-api   2/2     2            2           50m
rutas-norte-pre   bookings-api   2/2     2            2           3m

Pinning the context's namespace

This is the option that saves the most time. It modifies your kubeconfig so that every command uses that namespace by default:

kubectl config set-context --current --namespace=rutas-norte-pre
kubectl get pods
Context "rutas-norte" modified.

NAME                           READY   STATUS    RESTARTS   AGE
bookings-api-7f1d3e942-c9plk   1/1     Running   0          4m
bookings-api-7f1d3e942-r5vwj   1/1     Running   0          4m
redis-cache-6c8f9d745-h8nqz    1/1     Running   0          4m
web-store-6f9c4b8d7-d3jbx      1/1     Running   0          4m
web-store-6f9c4b8d7-m7kwr      1/1     Running   0          4m
web-store-6f9c4b8d7-p2fst      1/1     Running   0          4m

And the check that should become a reflex before any destructive command:

kubectl config view --minify -o jsonpath='{..namespace}{"\n"}'
rutas-norte-pre

A highly recommended tip: configure your terminal prompt to show the context and the namespace. Tools such as kube-ps1 do it, and seeing (rutas-norte:rutas-norte-pro) on screen before typing delete has saved many an afternoon.

kubens

Part of the kubectx suite, kubens switches namespace interactively:

kubens                       # lists the namespaces and marks the current one
kubens rutas-norte-pro       # switches to production
kubens -                     # goes back to the previous one

It is syntactic sugar over kubectl config set-context, but with an interactive listing and a quick jump back. It is installed as a standalone binary or through krew, the kubectl plugin manager we saw in The Kubernetes CLI.

Go back to development before moving on:

kubectl config set-context --current --namespace=rutas-norte-dev

  1. DNS across namespaces

You already know a Service's full name: <service>.<namespace>.svc.cluster.local. Namespaces are the reason that structure exists.

When a pod resolves a name, the cluster DNS tries a series of search suffixes in order. Look at them in a real pod:

kubectl exec deploy/bookings-api -- cat /etc/resolv.conf
search rutas-norte-dev.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

The first line explains the whole behaviour: when looking up redis-cache, the pod tries redis-cache.rutas-norte-dev.svc.cluster.local first. Since it exists, that is where it stops. A short name always resolves within its own namespace.

From a rutas-norte-dev pod, if it writes... It resolves to...
redis-cache redis-cache.rutas-norte-dev.svc.cluster.local
redis-cache.rutas-norte-pre redis-cache.rutas-norte-pre.svc.cluster.local
redis-cache.rutas-norte-pro.svc.cluster.local That very name, with no ambiguity

Check it:

kubectl run test-dns --rm -it --image=busybox:1.36 --restart=Never -n rutas-norte-dev -- \
  sh -c 'nslookup bookings-api; nslookup bookings-api.rutas-norte-pre'
Name:      bookings-api.rutas-norte-dev.svc.cluster.local
Address:   10.96.184.22

Name:      bookings-api.rutas-norte-pre.svc.cluster.local
Address:   10.96.229.14

pod "test-dns" deleted

There is good news and bad news here. The good: the same bookings-api manifest, with DATABASE_HOST: bookings-postgres, works in all three environments and each one talks to its own database, with no conditionals and no templating.

The bad, and it matters: a development pod has just resolved and could connect to a preproduction Service. Nothing stops it. That takes us straight to the next section.

  1. What a namespace does NOT guarantee

This is the section that prevents incidents. The belief that "each team in its namespace and they are therefore isolated" is dangerously false.

It is not a network boundary

By default, any pod in any namespace can connect to any pod or Service in any other namespace. Kubernetes' flat network model is explicit about this: every pod can see every other pod.

An uncomfortable demonstration: a development pod writing to the preproduction cache.

kubectl run intruder --rm -it --image=redis:7.2-alpine --restart=Never -n rutas-norte-dev -- \
  redis-cli -h redis-cache.rutas-norte-pre SET seats:BIL-SAN:2026-08-14 0
OK
pod "intruder deleted"

We have just set the available seats in the preproduction cache to zero from development. Had it been rutas-norte-pro, it would be a serious incident. And nobody needed special permissions: knowing the name was enough.

The solution is not the namespace, it is the NetworkPolicies of 04-06, which do let you declare rules of the kind "bookings-postgres only accepts connections from pods labelled app=bookings-api in the same namespace".

It is not a security or permissions boundary

Creating a namespace does not restrict who can do what inside it. If your credentials have broad permissions over the cluster, you have them over every namespace, existing and future.

kubectl auth can-i delete deployments -n rutas-norte-pro
yes

With an administrator user, the answer is yes for any namespace. Permission isolation comes from the Role and RoleBinding objects of RBAC. The namespace is only the scope those rules apply to: necessary, but not sufficient.

It does not limit resource consumption

A namespace imposes no ceiling. A badly configured Deployment in rutas-norte-dev can consume all the cluster's CPU and memory and starve the pods in rutas-norte-pro, because they share the same nodes.

kubectl describe namespace rutas-norte-dev
Name:         rutas-norte-dev
Labels:       app.kubernetes.io/part-of=rutas-norte
              environment=dev
Status:       Active

No resource quota.

No LimitRange resource.

Those two final lines say it all: no quota and no limits. Setting them is the subject of Resource Quotas and Limits and LimitRanges and QoS.

It does not isolate the nodes

Pods from every namespace are spread over the same nodes. A development pod and a production pod can be neighbours on the same machine, sharing CPU, memory and disk.

kubectl get pods -A -l app=web-store -o custom-columns=NS:.metadata.namespace,POD:.metadata.name,NODE:.spec.nodeName
NS                POD                         NODE
rutas-norte-dev   web-store-6f9c4b8d7-42kxr   rutas-norte
rutas-norte-pre   web-store-6f9c4b8d7-d3jbx   rutas-norte

Separating workloads by node requires taints, tolerations and affinity (06-05).

An honest summary

Common belief Reality
"Namespaces isolate the network" False. Everything sees everything. NetworkPolicies are needed
"Namespaces isolate permissions" False on their own. Roles and RoleBindings are needed
"One namespace cannot consume another's resources" False. A ResourceQuota is needed
"Production is protected by being in another namespace" False. It is an organisational separation, not a barrier
"Namespaces separate the nodes" False. Nodes are shared
"Namespaces prevent name collisions" True. That one is their native function

The sentence that sums up the section: a namespace is the scope the isolation mechanisms apply to, not the isolation mechanism. Without quotas, RBAC and network policies on top, a namespace is just a folder.

  1. Deleting a namespace: the most dangerous kubectl command

kubectl delete namespace rutas-norte-pre

That command deletes everything inside it: Deployments, ReplicaSets, Pods, Services, ConfigMaps, Secrets, PersistentVolumeClaims, ServiceAccounts, Roles... And with the PVCs go, depending on the reclaim policy, the data.

Traits that make it especially dangerous:

  • It asks for no confirmation. None.
  • There is no undo. There is no bin and no kubectl undelete.
  • It is asynchronous. The namespace goes into Terminating while the contents are deleted, and in that state it accepts no new objects.
  • A typo is catastrophic. rutas-norte-pro instead of rutas-norte-pre is three characters apart.

See it in action, with a throwaway namespace:

kubectl create namespace delete-test
kubectl run temp --image=nginx:1.27-alpine -n delete-test
kubectl delete namespace delete-test
kubectl get namespace delete-test
namespace/delete-test created
pod/temp created
namespace "delete-test" deleted

Error from server (NotFound): namespaces "delete-test" not found

The pod went with it, without asking anything.

Namespaces stuck in Terminating

A problem you will hit sooner or later: a namespace that stays in Terminating indefinitely. The cause is almost always a finalizer —those hooks we studied in Objects and Manifests— that cannot complete, typically one belonging to an extended API whose server no longer exists.

kubectl get namespace rutas-norte-pre -o jsonpath='{.spec.finalizers}{"\n"}'
kubectl get namespace rutas-norte-pre -o jsonpath='{.status.conditions}' | python3 -m json.tool

Before forcing anything, work out which resource is blocking it, because ripping the finalizer out leaves orphaned objects in etcd. The correct approach is to remove the offending resource or restore the service that is supposed to process it.

Protective measures for Rutas Norte

  1. Label and annotate production, as we did in namespaces.yaml: describing the namespace shows the notice.
  2. Restrictive RBAC: nobody has delete namespaces permission on rutas-norte-pro except a couple of people (08-01).
  3. Verify the context before any deletion, with the reflex from section 6.
  4. Backups: the only real safety net. Tools such as Velero can restore an entire namespace, and that is the subject of Backup and Restore.
  5. Check before firing, always:
kubectl get all -n rutas-norte-pre

One extra good habit: use --dry-run=client mentally. Before deleting, ask yourself out loud which namespace you are looking at.

Leave preproduction standing, we will keep using it:

kubectl get ns -l app.kubernetes.io/part-of=rutas-norte
NAME              STATUS   AGE
rutas-norte-dev   Active   2d
rutas-norte-pre   Active   25m
rutas-norte-pro   Active   25m

  1. Namespaces versus separate clusters

The strategic question: should Rutas Norte's three environments live in one cluster with three namespaces, or in three separate clusters?

Criterion Namespaces in one cluster Separate clusters
Cost One control plane, shared nodes: far cheaper Multiplied by the number of clusters
Real isolation Logical; requires well-built quotas, RBAC and NetworkPolicies Total: different infrastructure
Blast radius of a failure A control plane problem affects everything A cluster going down does not touch the others
Kubernetes version The same for every environment One each: an upgrade can be tested in pre
Operational complexity Low: one kubeconfig, one toolset High: it multiplies monitoring, upgrades and access
Risk of human error High: a misplaced -n touches production Low: switching cluster is a conscious act
Regulatory compliance Hard to justify to an auditor Easy to justify
Latency between environments None, they share the network Requires external connectivity

Practical decision criteria:

Namespaces in one cluster when:

  • The environments are of equivalent trust (dev and pre from the same team).
  • The team is small and the budget limited.
  • RBAC, quotas and network policies are well understood.

Separate clusters when:

  • There is regulated data: personal, health or financial.
  • Service is offered to customers who do not trust each other (real multi-tenancy).
  • Different versions of Kubernetes or of cluster components are needed.
  • A production failure cannot depend on the health of a test environment.

Rutas Norte's decision

The company holds personal customer data —name, ID number, phone and email— in bookings-postgres. The decision, and its rationale:

Environment Location Justification
rutas-norte-dev A namespace in the non-production cluster Anonymised data, minimal cost, fast iteration
rutas-norte-pre A namespace in the same cluster as dev A functional replica of production with fictional data
rutas-norte-pro Its own separate cluster Real personal data, availability and audit requirements, contained blast radius

In other words: two clusters, three namespaces. The rutas-norte-pro namespace also exists in the non-production cluster so the manifests are identical and the rollout can be rehearsed, but real production lives apart.

Throughout the course we will work with a single minikube and the three namespaces, which is the practical choice for learning. Managing several clusters at once, with their contexts and tooling, is covered in Multi-Cluster Management.

One last note on how to organise namespaces: by environment (what we do), by team (payments-team, routes-team) or by application (rutas-norte, intranet). The usual approach is to combine two axes: rutas-norte-pro, intranet-pro. What never works is one namespace per microservice: it multiplies the bureaucracy without providing useful isolation.

Common Mistakes and Tips

  • Deploying to default by forgetting. The symptom is kubectl get pods returning nothing where you expected results. Check with kubectl get pods -A -l app=<component>.
  • Believing a namespace isolates the network. It does not. Any pod can connect to any Service in another namespace if it knows its name.
  • Believing a namespace isolates permissions or resources. RBAC and ResourceQuota are needed. The namespace is the scope, not the mechanism.
  • Passing -n to a cluster-scoped resource. kubectl get nodes -n whatever silently ignores the flag and can mislead you.
  • A mismatch between metadata.namespace and -n. kubectl errors out instead of deciding for you. It is a protection, not an annoyance.
  • Searching in the wrong namespace and concluding something does not exist. When in doubt, -A.
  • Deleting a namespace without looking at what it contains. There is no confirmation and no undo. kubectl get all -n <ns> first, always.
  • Forcing the deletion of a stuck namespace by stripping its finalizers. It leaves orphaned objects in etcd. Investigate what is blocking it first.
  • One namespace per microservice. It multiplies the administrative load without providing real isolation. Organise by environment, team or application.
  • Tip: configure the prompt to show context and namespace (kube-ps1). Seeing (rutas-norte:rutas-norte-pro) before typing delete prevents accidents.
  • Tip: declare metadata.namespace in the manifests that are specific to an environment and omit it in the generic ones. Ambiguity is what causes deployments in the wrong place.
  • Tip: always label your namespaces. Module 4's NetworkPolicies will select namespaces by label, and without them you will have to go back and add them.

Exercises

Exercise 1: Classify resources and explore the system

  1. With a single command, count how many resource types are namespaced and how many are not.
  2. Determine, without consulting tables, whether these resources are namespaced: PersistentVolume, PersistentVolumeClaim, Role, ClusterRole, Ingress, StorageClass.
  3. List the kube-system pods and identify which are the five control plane components you studied in module 1.
  4. Find out what is inside kube-public and explain what is special about that namespace.

Exercise 2: Deploy the platform to production and check the isolation

  1. Deploy web-store (Deployment and Service) to rutas-norte-pro, with the label environment: pro instead of dev.
  2. Prove with a single command that three Deployments called web-store exist in the cluster, in different namespaces.
  3. Check that the ClusterIPs of the three web-store Services are different.
  4. From an ephemeral pod in rutas-norte-dev, make an HTTP request to the web-store in rutas-norte-pro using the full DNS name. Does it work? Explain what this implies and which object would have to be created to prevent it.

Exercise 3: A namespace audit

You join Rutas Norte's platform team and are asked for a quick audit of the cluster. Answer with commands and their output:

  1. Which namespaces exist and which of them belong to the Rutas Norte platform?
  2. Is there any object deployed in default? If there is, move it to the correct namespace.
  3. Which namespaces have a ResourceQuota defined? What does it imply that they do not?
  4. Can you delete the production namespace with your current credentials? What mechanism ought to prevent it and in which lesson is it studied?
  5. Draw up a table with the three environments showing: number of pods, number of Services and whether they have a quota.

Solutions

Solution 1

echo "Namespaced: $(kubectl api-resources --namespaced=true --no-headers | wc -l)"
echo "Cluster-scoped: $(kubectl api-resources --namespaced=false --no-headers | wc -l)"
Namespaced: 43
Cluster-scoped: 26

The numbers vary with the CRDs and addons installed in your cluster.

  1. The reasoning, before checking it: anything belonging to a specific application is namespaced; anything that is infrastructure or global configuration is not.
Resource Namespaced? Reasoning
PersistentVolume No It is a cluster disk, it exists before anybody claims it
PersistentVolumeClaim Yes It is a specific application's request
Role Yes It grants permissions within a namespace
ClusterRole No It grants permissions across the whole cluster
Ingress Yes It routes to Services, which are namespaced
StorageClass No It defines a storage type available to the whole cluster
kubectl api-resources --no-headers | grep -E "^(persistentvolumes|persistentvolumeclaims|roles|clusterroles|ingresses|storageclasses) "
persistentvolumeclaims  pvc   v1                             true    PersistentVolumeClaim
persistentvolumes       pv    v1                             false   PersistentVolume
storageclasses          sc    storage.k8s.io/v1              false   StorageClass
ingresses               ing   networking.k8s.io/v1           true    Ingress
clusterroles                  rbac.authorization.k8s.io/v1   false   ClusterRole
roles                         rbac.authorization.k8s.io/v1   true    Role
# 3. Control plane components
kubectl get pods -n kube-system --no-headers | awk '{print $1}' | grep -E "apiserver|etcd|scheduler|controller-manager|coredns"
coredns-668d6bf9bc-x8k2m
etcd-rutas-norte
kube-apiserver-rutas-norte
kube-controller-manager-rutas-norte
kube-scheduler-rutas-norte

The five: kube-apiserver (the only door to the API), etcd (the state store), kube-scheduler (assigns pods to nodes), kube-controller-manager (where the ReplicaSet, Deployment and endpoints controllers live) and CoreDNS (which resolves the Service names we used in the previous lesson).

# 4. kube-public
kubectl get configmaps -n kube-public
kubectl get configmap cluster-info -n kube-public -o jsonpath='{.data.jws-kubeconfig-*}' | head -3
NAME               DATA   AGE
cluster-info       2      3d
kube-root-ca.crt   1      3d

kube-public is the only namespace readable without authenticating: anybody who can reach the apiserver can read its contents. It holds cluster-info, with the data a node needs to join the cluster during the kubeadm join process. That is why nothing sensitive must ever be kept there.

Solution 2

# 1. Deploy to production
for f in web-store-deployment web-store-service; do
  sed -e 's/rutas-norte-dev/rutas-norte-pro/g' -e 's/environment: dev/environment: pro/g' \
    k8s/base/$f.yaml | kubectl apply -f -
done
kubectl get deploy,svc -n rutas-norte-pro
deployment.apps/web-store created
service/web-store created

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web-store   3/3     3            3           15s

NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/web-store   ClusterIP   10.96.171.55    <none>        80/TCP    15s
# 2. The three Deployments
kubectl get deploy web-store -A
NAMESPACE         NAME        READY   UP-TO-DATE   AVAILABLE   AGE
rutas-norte-dev   web-store   3/3     3            3           1h
rutas-norte-pre   web-store   3/3     3            3           35m
rutas-norte-pro   web-store   3/3     3            3           1m
# 3. Three different ClusterIPs
kubectl get svc web-store -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,IP:.spec.clusterIP
NS                NAME        IP
rutas-norte-dev   web-store   10.96.12.209
rutas-norte-pre   web-store   10.96.44.71
rutas-norte-pro   web-store   10.96.171.55
# 4. From dev towards pro
kubectl run intruder --rm -it --image=curlimages/curl:8.8.0 --restart=Never -n rutas-norte-dev -- \
  curl -s -o /dev/null -w "%{http_code}\n" http://web-store.rutas-norte-pro.svc.cluster.local/
200
pod "intruder" deleted

It works. A development pod has reached a production service without the slightest obstacle. The direct implication: a namespace is not a network boundary. In Kubernetes' default model, every pod can see every other pod, and knowing the DNS name is enough to connect.

To prevent it you need a NetworkPolicy in rutas-norte-pro that rejects all inbound traffic except what comes from namespaces labelled environment: pro (which is why we labelled the namespaces when creating them). That is the content of Network Policies and Network Security.

Solution 3

# 1. Existing namespaces and the platform ones
kubectl get ns
kubectl get ns -l app.kubernetes.io/part-of=rutas-norte
NAME              STATUS   AGE
default           Active   3d
kube-node-lease   Active   3d
kube-public       Active   3d
kube-system       Active   3d
rutas-norte-dev   Active   2d
rutas-norte-pre   Active   40m
rutas-norte-pro   Active   40m

NAME              STATUS   AGE
rutas-norte-dev   Active   2d
rutas-norte-pre   Active   40m
rutas-norte-pro   Active   40m

The shared label makes it possible to tell the platform namespaces from the system ones at a glance.

# 2. Objects in default
kubectl get all -n default
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   3d

Only service/kubernetes shows up, which is the apiserver's own Service and must be there: it is what lets pods talk to the API. If there were anything else, the procedure would be to export it, correct its namespace and apply it again:

kubectl get deploy <name> -n default -o yaml > /tmp/object.yaml
# edit metadata.namespace and remove metadata.uid, resourceVersion and status
kubectl apply -f /tmp/object.yaml -n rutas-norte-dev
kubectl delete deploy <name> -n default
# 3. Quotas
kubectl get resourcequota -A
No resources found

No namespace has a quota. Implication: any Deployment in rutas-norte-dev can consume all the cluster's CPU and memory and starve the pods in rutas-norte-pro, because they share nodes. A badly written loop in development can bring production down. It is fixed with ResourceQuota and LimitRange in 03-04 and 03-05.

# 4. Deletion permissions
kubectl auth can-i delete namespace rutas-norte-pro
yes

Yes, I can delete it, and with it all of production, with no confirmation and no undo. What ought to prevent it is RBAC: a Role/ClusterRole that does not include the delete verb on namespaces for everyday credentials, leaving that permission to an emergency role used by two people. It is studied in Role-Based Access Control. As a complementary safety net, backups with Velero (05-06).

# 5. Summary table
for ns in rutas-norte-dev rutas-norte-pre rutas-norte-pro; do
  pods=$(kubectl get pods -n $ns --no-headers 2>/dev/null | wc -l)
  svcs=$(kubectl get svc -n $ns --no-headers 2>/dev/null | wc -l)
  quota=$(kubectl get resourcequota -n $ns --no-headers 2>/dev/null | wc -l)
  echo "$ns | $pods pods | $svcs services | quotas: $quota"
done
rutas-norte-dev | 7 pods | 4 services | quotas: 0
rutas-norte-pre | 6 pods | 3 services | quotas: 0
rutas-norte-pro | 3 pods | 1 services | quotas: 0
Environment Pods Services Quota? Observation
rutas-norte-dev 7 4 No A complete environment: all four components deployed
rutas-norte-pre 6 3 No bookings-postgres is missing
rutas-norte-pro 3 1 No Only web-store; with no quota it is the cluster's most serious risk

Conclusion

Namespaces have stopped being that word we repeated in every command. You know their native function is name uniqueness, and that from it follow the scope of queries, of selectors, of short DNS names, of quotas, of RBAC and of network policies. You tell namespaced resources —pods, Deployments, Services, ConfigMaps, Secrets, PVCs— apart from those belonging to the whole cluster —nodes, PersistentVolumes, StorageClasses, ClusterRoles—, and you know how to find out at any moment with kubectl api-resources --namespaced. You know the four system namespaces, you have seen the control plane running as pods in kube-system, and you are clear about why default is a dumping ground where Rutas Norte deploys nothing.

You have set up the project's three environments declaratively and with labels the NetworkPolicies will exploit later, and you have deployed the same manifests to several of them, confirming that web-store and bookings-api coexist under identical names in different namespaces, each with its own ClusterIP and talking to its own database thanks to short DNS names. You work with -n, with -A and by pinning the context's namespace, with the reflex of checking where you are before any destructive command.

And you take away the two warnings that prevent the most incidents. The first: a namespace isolates nothing on its own. It is not a network boundary —you proved it by writing to the preproduction cache from development and calling production from a dev pod—, it is not a permissions boundary, it does not limit resources and it does not separate nodes. It is the scope that quotas, RBAC and network policies apply to, and those arrive in modules 3, 4 and 8. The second: kubectl delete namespace takes everything with it, with no confirmation and no way back, which is why Rutas Norte's real production lives in a separate cluster.

One loose thread has run through the whole module. The ReplicaSets' selectors, the Services' selectors, the namespaces' environment label, the kubernetes.io/change-cause annotation in the revision history, the pod-template-hash the Deployment adds by itself, the -l queries we have been using since the first lesson... All of those are labels and annotations, and so far we have been using them without any system. In Labels, Selectors and Annotations, the lesson that closes the module, we will put them in order: syntax and constraints, the labels Kubernetes recommends, the definitive labelling scheme for the six Rutas Norte components across their three environments, equality- and set-based selectors, and the everyday queries that turn a large cluster into something navigable.

Kubernetes Course

Module 1: Introduction to Kubernetes

Module 2: Core Kubernetes Components

Module 3: Configuration and Secret Management

Module 4: Networking in Kubernetes

Module 5: Storage in Kubernetes

Module 6: Advanced Kubernetes Concepts

Module 7: Monitoring and Logging

Module 8: Kubernetes Security

Module 9: Scaling and Performance

Module 10: Kubernetes Ecosystem and Tooling

Module 11: Case Studies and Real-World Applications

Module 12: Preparing for Kubernetes Certification

© Copyright 2026. All rights reserved