Namespaces & Labels
Organising, isolating, and selecting Kubernetes resources through Helm templates
1. Why Namespaces Matter
A Kubernetes cluster can run hundreds of workloads from different teams, environments, and projects. Without some form of partitioning, name collisions are inevitable and access control becomes difficult. Namespaces solve both problems.
A namespace is a virtual cluster inside a physical cluster. Resources within a namespace must have unique names, but two different namespaces can each contain a Deployment called api without conflict. Beyond naming, namespaces serve as the primary boundary for:
- Resource quotas — cap the total CPU, memory, and pod count a namespace may consume.
- RBAC — RoleBindings scope permissions to a single namespace.
- Network policies — rules can allow or deny traffic between namespaces.
Every cluster ships with four built-in namespaces: default, kube-system, kube-public, and kube-node-lease. In practice, you should avoid deploying application workloads into default because it receives no quota by default and makes multi-tenant operations harder.
2. Namespaces in Helm
When you run helm install, the -n (or --namespace) flag tells Helm which namespace to target. Combined with --create-namespace, Helm will create the namespace if it does not already exist:
helm install my-release ./mychart -n staging --create-namespace
Inside your templates, every resource automatically receives the target namespace. You only need to use the built-in object {{ .Release.Namespace }} when the namespace value must appear as data (for example, in a ConfigMap or an environment variable), or when you are creating cross-namespace resources.
Including a Namespace Resource in the Chart
Sometimes you want the chart itself to manage the namespace — for example, when it needs specific labels, annotations, or an attached ResourceQuota. A common pattern is to gate the resource behind a values flag:
# templates/namespace.yaml
{{- if .Values.namespace.create }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Release.Namespace }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
annotations:
purpose: {{ .Values.namespace.purpose | default "general" | quote }}
{{- end }}
# values.yaml
namespace:
create: false
purpose: "team-backend"
When namespace.create is true, the chart manages the namespace object. When false (the default), the chart assumes the namespace already exists — the operator creates it out of band or via --create-namespace.
Design Tip
Default to create: false. In production clusters, namespace creation is usually handled by a platform team or a GitOps controller, not individual application charts.
3. Resource Quotas and LimitRanges
A ResourceQuota caps the aggregate resource consumption of all pods in a namespace. A LimitRange sets default and maximum values for individual containers. Together, they prevent any single team or application from starving the cluster.
# templates/resourcequota.yaml
{{- if .Values.resourceQuota.enabled }}
apiVersion: v1
kind: ResourceQuota
metadata:
name: {{ .Release.Name }}-quota
spec:
hard:
requests.cpu: {{ .Values.resourceQuota.cpu | quote }}
requests.memory: {{ .Values.resourceQuota.memory | quote }}
limits.cpu: {{ .Values.resourceQuota.cpuLimit | quote }}
limits.memory: {{ .Values.resourceQuota.memoryLimit | quote }}
pods: {{ .Values.resourceQuota.pods | quote }}
{{- end }}
# templates/limitrange.yaml
{{- if .Values.limitRange.enabled }}
apiVersion: v1
kind: LimitRange
metadata:
name: {{ .Release.Name }}-limits
spec:
limits:
- type: Container
default:
cpu: {{ .Values.limitRange.defaultCpu | quote }}
memory: {{ .Values.limitRange.defaultMemory | quote }}
defaultRequest:
cpu: {{ .Values.limitRange.requestCpu | quote }}
memory: {{ .Values.limitRange.requestMemory | quote }}
{{- end }}
4. Labels: Purpose and Conventions
Labels are key/value strings attached to any Kubernetes object. Unlike annotations, labels are indexed by the API server, which means you can query, filter, and select resources based on their labels.
The Kubernetes documentation recommends a set of common labels under the app.kubernetes.io prefix:
| Label | Purpose | Example |
|---|---|---|
app.kubernetes.io/name | Application name | myapp |
app.kubernetes.io/instance | Unique release instance | myapp-staging |
app.kubernetes.io/version | Application version | 1.4.0 |
app.kubernetes.io/component | Component within architecture | frontend |
app.kubernetes.io/part-of | Higher-level application | ecommerce |
app.kubernetes.io/managed-by | Tool managing the resource | Helm |
Following this convention ensures that tools such as kubectl, Prometheus, Grafana dashboards, and service meshes can automatically group and filter your workloads.
5. Annotations vs Labels
Both are key/value pairs in metadata, but they serve different roles:
- Labels are for identification and selection. They drive
matchLabelsin Deployments, Services, and Jobs. Values must be 63 characters or fewer. - Annotations are for non-identifying metadata. They store configuration for tooling — Prometheus scrape targets, Ingress controller hints, CI/CD build URLs, and more. Values can be arbitrarily large.
Rule of Thumb
If you will never need to kubectl get ... -l key=value on it, use an annotation. If you need to select or filter, use a label.
6. Label Selectors in Services and Deployments
Labels only become powerful when combined with selectors. A Service uses spec.selector to discover which pods receive traffic. A Deployment uses spec.selector.matchLabels to know which pods it owns.
# Deployment selector (immutable after creation)
spec:
selector:
matchLabels:
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
# Must include everything in matchLabels, plus optional extras
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
The pod template's labels must be a superset of matchLabels. You can include additional labels on the pods (like version), but the selector labels must always be present.
Important
Once a Deployment is created, its selector.matchLabels cannot be changed. If you need to change selector labels, you must delete and recreate the Deployment. This is a common source of failed helm upgrade operations.
7. Centralising Labels with _helpers.tpl
Repeating the same label block in every template is error-prone. Helm's convention is to define reusable named templates in templates/_helpers.tpl:
{{/* Common labels */}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/* Selector labels (subset used in matchLabels) */}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Every template then simply calls the helper:
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
This pattern — splitting full labels from selector labels — is the Helm community standard. The helm create scaffolding generates it by default. By following it, you ensure that selector labels remain stable (they never include version or chart), while informational labels can evolve freely.
8. Best Practices
- One namespace per environment per app — e.g.,
myapp-staging,myapp-prod. This cleanly separates quotas and RBAC. - Always use
app.kubernetes.io/*labels. They are the de facto standard understood by the entire ecosystem. - Keep selector labels minimal and stable. Only
nameandinstancebelong inmatchLabels; never include version or chart metadata. - Centralise in
_helpers.tpl. A single source of truth for labels eliminates drift between resources. - Apply ResourceQuotas in production. Without quotas, a misconfigured Deployment can consume all cluster resources.
- Use annotations for tooling configuration. Prometheus scrape settings, Ingress hints, and CI metadata are annotations, not labels.
Summary
Namespaces provide isolation and governance boundaries; labels provide the selection and filtering mechanism that ties Kubernetes resources together. When managed through Helm templates — with helpers for consistency and values for flexibility — they form a robust metadata strategy that scales from a single chart to a platform-wide deployment system. In the next lesson, we will build on this foundation by looking at ServiceAccounts and RBAC, which use namespaces as their primary scope.