Day: November 23, 2025

How to Deploy a Kubernetes Application with a Clean Namespace Structure

How to Deploy a Kubernetes Application with a Clean Namespace Structure

When you deploy an application to Kubernetes in production, you shouldn’t throw everything into the default namespace or a single giant YAML file. A proper setup uses:

  • A dedicated namespace for the app
  • A ServiceAccount and RBAC for security
  • ConfigMap and Secret for configuration
  • Deployment, Service, and Ingress for runtime and traffic
  • HPA, PDB, and NetworkPolicies for reliability and security

    HPA vs PDB (Summary)

    ========================

    Feature HPA PDB
    Scales pods based on load ✔ YES ❌ NO
    Ensures minimum pods stay up ❌ NO ✔ YES
    Helps with traffic spikes ✔ YES ❌ NO
    Protects during upgrades/drains ❌ NO ✔ YES
    Operates on load (CPU/metrics) ✔ YES ❌ NO
    Operates on disruptions ❌ NO ✔ YES
    Controls min/max replicas ✔ YES ❌ NO
    Controls disruption limits ❌ NO ✔ YES

In this post, we’ll walk through a clean, real-world Kubernetes namespace structure, show the YAML for each section, and explain what it does. You can drop all of these files into a directory and apply them in one go with:

kubectl apply -f k8s/

1. Directory Structure

Create a folder for your Kubernetes manifests, for example:

k8s/
  namespace.yaml
  serviceaccount.yaml
  rbac.yaml
  configmap.yaml
  secret.yaml
  deployment.yaml
  service.yaml
  ingress.yaml
  hpa.yaml
  pdb.yaml
  networkpolicy-default-deny.yaml
  networkpolicy-allow-ingress.yaml

Kubernetes will treat all of these files as one desired state when you run kubectl apply -f k8s/, similar to how Terraform reads multiple .tf files in one directory.


2. Namespace – Isolating the Application

A namespace is a logical boundary in the cluster. Think of it as a dedicated “folder” for your application’s resources.

apiVersion: v1
kind: Namespace
metadata:
  name: prod-app
  labels:
    name: prod-app
    pod-security.kubernetes.io/enforce: "restricted"
    pod-security.kubernetes.io/enforce-version: "latest"

What this does:

  • Creates a namespace called prod-app.
  • Applies Pod Security labels to enforce restricted policies.
  • Gives you a clean way to separate dev, staging, and prod environments.

3. ServiceAccount – Identity for the Pods

A ServiceAccount represents the identity your pods use inside Kubernetes. Instead of relying on the default ServiceAccount, you create a dedicated one for your app.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: prod-app

What this does:

  • Creates a ServiceAccount named app-sa in the prod-app namespace.
  • Your Deployment will run pods using this identity, not the insecure default.

4. RBAC – Roles and RoleBindings

RBAC (Role-Based Access Control) defines what your application is allowed to do inside the namespace. You don’t want your app to have full cluster access; you give it just enough permissions.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-read-config
  namespace: prod-app
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-read-config-binding
  namespace: prod-app
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: prod-app
roleRef:
  kind: Role
  name: app-read-config
  apiGroup: rbac.authorization.k8s.io

What this does:

  • Role app-read-config:
    • Allows reading (get, list) ConfigMaps and Secrets in this namespace.
  • RoleBinding:
    • Attaches that Role to the app-sa ServiceAccount.
    • Any pod running as app-sa can now read ConfigMaps and Secrets in prod-app.

5. ConfigMap – Non-Sensitive Configuration

A ConfigMap holds non-secret configuration such as runtime flags, modes, switches, or log levels.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: prod-app
data:
  APP_ENV: "production"
  APP_LOG_LEVEL: "info"

What this does:

  • Stores plain-text configuration for your application.
  • Lets you change behavior without rebuilding the container image.

6. Secret – Sensitive Configuration

Secrets hold confidential settings such as database URLs, API keys, and credentials.

apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: prod-app
type: Opaque
stringData:
  DATABASE_URL: "postgres://user:password@db.prod:5432/app"
  API_KEY_EXTERNAL_SERVICE: "replace-me"

What this does:

  • Stores sensitive data separately from code.
  • Works with RBAC so only the right ServiceAccount can read it.

7. Deployment – The Application Workload

The Deployment defines how your containers run: image, replicas, health checks, resources, and security context. This is the core of your application’s runtime behavior.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
  namespace: prod-app
  labels:
    app: my-app
spec:
  replicas: 3
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      serviceAccountName: app-sa
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
        - name: app
          image: your-registry/your-image:TAG
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              name: http
          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: app-secret
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /livez
              port: http
            initialDelaySeconds: 15
            periodSeconds: 20
          securityContext:
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
      terminationGracePeriodSeconds: 30

What this does:

  • Runs 3 replicas of your application for availability.
  • Uses a safe rolling update strategy with zero downtime (maxUnavailable: 0, maxSurge: 1).
  • Runs pods under the app-sa ServiceAccount, inheriting its RBAC permissions.
  • Injects configuration from ConfigMap and Secret.
  • Defines health checks (readinessProbe, livenessProbe) so Kubernetes knows when to route traffic and when to restart pods.
  • Applies strict security settings (non-root user, no privilege escalation, read-only root filesystem).

8. Service – Internal Load Balancer

A Service provides a stable, cluster-internal endpoint to reach your pods.

apiVersion: v1
kind: Service
metadata:
  name: app-service
  namespace: prod-app
  labels:
    app: my-app
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - name: http
      port: 80
      targetPort: http

What this does:

  • Maps port 80 on the Service to port 8080 on the pods (via the named port http).
  • Provides stable DNS: app-service.prod-app.svc.cluster.local.
  • Load balances traffic across all healthy pods with app: my-app.

9. Ingress – External HTTP/HTTPS Access

Ingress exposes your Service to the outside world using a hostname and optional TLS.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: prod-app
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts:
        - app.your-domain.com
      secretName: app-tls-secret
  rules:
    - host: app.your-domain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-service
                port:
                  number: 80

What this does:

  • Routes traffic from https://app.your-domain.com to app-service on port 80.
  • Uses app-tls-secret for TLS termination (usually created by cert-manager).
  • Relies on an Ingress controller (e.g., NGINX) running in the cluster.

10. Horizontal Pod Autoscaler (HPA) – Scaling on Load

The HPA automatically adjusts the number of replicas based on metrics like CPU usage.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: app-hpa
  namespace: prod-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: app-deployment
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

What this does:

  • Keeps at least 3 pods running, and can scale up to 10.
  • Targets the app-deployment Deployment.
  • Scales based on CPU usage (e.g., above 60% average).

11. PodDisruptionBudget (PDB) – Protecting Availability

A PodDisruptionBudget ensures that voluntary disruptions (node drains, upgrades) don’t take down too many pods at once.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
  namespace: prod-app
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: my-app

What this does:

  • Guarantees that at least 2 pods are always available.
  • Protects your app during maintenance and cluster upgrades.

12. Network Policies – Zero-Trust Networking

By default, Kubernetes allows every pod to talk to every other pod. NetworkPolicies let you move to a zero-trust model.

Default Deny Policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod-app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

What this does:

  • Blocks all ingress and egress traffic for all pods in the namespace, unless explicitly allowed.

Allow Traffic from Ingress Controller to the App

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-ingress
  namespace: prod-app
spec:
  podSelector:
    matchLabels:
      app: my-app
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
      ports:
        - protocol: TCP
          port: 80
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: TCP
          port: 5432
        - protocol: TCP
          port: 443

What this does:

  • Allows only the Ingress controller namespace (e.g. ingress-nginx) to send HTTP traffic to the app.
  • Restricts egress traffic to specific ports (e.g., PostgreSQL and HTTPS).

Applying Everything

Once you have all these files in your k8s/ directory, you deploy the entire application stack with:

kubectl apply -f k8s/

Kubernetes reads all files, builds the state internally, and creates or updates resources in the correct order, very similar to how Terraform applies all .tf files in a directory.


Conclusion

A production-ready Kubernetes deployment is not just a Deployment and a Service. It is a structured set of manifests that cover identity, security, configuration, scaling, networking, and reliability.

  • Namespace – isolates your application.
  • ServiceAccount + RBAC – define identity and permissions.
  • ConfigMap + Secret – handle configuration and sensitive data.
  • Deployment + Service + Ingress – run the app and expose it.
  • HPA + PDB – keep it scalable and resilient.
  • NetworkPolicies – secure communication with a zero-trust model.

With this structure in place, you have a clean, repeatable Kubernetes deployment that fits naturally into Git, CI/CD, and GitOps workflows.

0