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
- What a namespace is and what it isolates
- Namespaced and cluster-scoped resources
- The system namespaces and why nothing is deployed to
default - The three Rutas Norte environments
- The same manifest in several environments
- Working with namespaces without going mad
- DNS across namespaces
- What a namespace does NOT guarantee
- Deleting a namespace: the most dangerous kubectl command
- Namespaces versus separate clusters
- 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.
- 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:
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 StatefulSetNAME 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 VolumeAttachmentThe 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:
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.
- The system namespaces and why nothing is deployed to
default
defaultEvery cluster is born with four 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:
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 3dThere 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.
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:
- It is the cluster's dumping ground. Everything anybody applies without
-nends up there. Within a few months it is a junk drawer where nobody knows what belongs to whom. - 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. - Accidents are easy. A
kubectl delete deploy --allwithout-n, run in the belief you were somewhere else, takes down whatever is indefault. - It makes isolation impossible. If everything is in
default, you cannot apply per-team quotas, per-environment RBAC or differentiated network policies. - It prevents reusing names. With everything in one namespace, the development and production
bookings-apicannot coexist.
The project rule is explicit:
At Rutas Norte,
defaultis forbidden. Every object lives in a named namespace, declared in its own manifest.
- 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-nortenamespace/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 4sNote two deliberate decisions:
- The namespaces carry labels. This is not decorative: in module 4, NetworkPolicies will select namespaces by the
environmentlabel, 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-prowill 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.
- 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-nortekubectl apply -f k8s/base/web-store-deployment.yaml -n rutas-norte-pre
kubectl apply -f k8s/base/web-store-service.yaml -n rutas-norte-preOption 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.
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-preNAME 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 26sNow look at the cluster's full situation:
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 1mTwo 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"}'Same name, two different objects, two different virtual IPs.
- 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:
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 3mPinning 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:
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 4mAnd the check that should become a reflex before any destructive command:
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 oneIt 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:
- 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:
search rutas-norte-dev.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5The 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" deletedThere 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.
- 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 0We 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.
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.
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.nodeNameNS POD NODE
rutas-norte-dev web-store-6f9c4b8d7-42kxr rutas-norte
rutas-norte-pre web-store-6f9c4b8d7-d3jbx rutas-norteSeparating 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.
- Deleting a namespace: the most dangerous kubectl command
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
Terminatingwhile the contents are deleted, and in that state it accepts no new objects. - A typo is catastrophic.
rutas-norte-proinstead ofrutas-norte-preis 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-testnamespace/delete-test created
pod/temp created
namespace "delete-test" deleted
Error from server (NotFound): namespaces "delete-test" not foundThe 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.toolBefore 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
- Label and annotate production, as we did in
namespaces.yaml: describing the namespace shows the notice. - Restrictive RBAC: nobody has
delete namespacespermission onrutas-norte-proexcept a couple of people (08-01). - Verify the context before any deletion, with the reflex from section 6.
- Backups: the only real safety net. Tools such as Velero can restore an entire namespace, and that is the subject of Backup and Restore.
- Check before firing, always:
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:
- 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
defaultby forgetting. The symptom iskubectl get podsreturning nothing where you expected results. Check withkubectl 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
-nto a cluster-scoped resource.kubectl get nodes -n whateversilently ignores the flag and can mislead you. - A mismatch between
metadata.namespaceand-n.kubectlerrors 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 typingdeleteprevents accidents. - Tip: declare
metadata.namespacein 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
- With a single command, count how many resource types are namespaced and how many are not.
- Determine, without consulting tables, whether these resources are namespaced:
PersistentVolume,PersistentVolumeClaim,Role,ClusterRole,Ingress,StorageClass. - List the
kube-systempods and identify which are the five control plane components you studied in module 1. - Find out what is inside
kube-publicand explain what is special about that namespace.
Exercise 2: Deploy the platform to production and check the isolation
- Deploy
web-store(Deployment and Service) torutas-norte-pro, with the labelenvironment: proinstead ofdev. - Prove with a single command that three Deployments called
web-storeexist in the cluster, in different namespaces. - Check that the
ClusterIPs of the threeweb-storeServices are different. - From an ephemeral pod in
rutas-norte-dev, make an HTTP request to theweb-storeinrutas-norte-prousing 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:
- Which namespaces exist and which of them belong to the Rutas Norte platform?
- Is there any object deployed in
default? If there is, move it to the correct namespace. - Which namespaces have a
ResourceQuotadefined? What does it imply that they do not? - Can you delete the production namespace with your current credentials? What mechanism ought to prevent it and in which lesson is it studied?
- 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)"The numbers vary with the CRDs and addons installed in your cluster.
- 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-norteThe 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 -3kube-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-prodeployment.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 15sNAMESPACE 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.clusterIPNS 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/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-norteNAME 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 40mThe shared label makes it possible to tell the platform namespaces from the system ones at a glance.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3dOnly 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 defaultNo 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.
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"
donerutas-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
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
