Back to Blog
4 min read

Kubernetes Adoption in the Enterprise: Lessons from 2021

Kubernetes moved from early adopter to mainstream enterprise technology in 2021. But with broader adoption came harder lessons. Here’s what we learned about running Kubernetes at scale this year.

The Managed Kubernetes Reality

Most enterprises settled on managed Kubernetes. Azure Kubernetes Service (AKS) became the default choice for Azure shops:

# Production-ready AKS cluster - 2021 best practices
az aks create \
    --resource-group production-rg \
    --name prod-aks-cluster \
    --node-count 5 \
    --node-vm-size Standard_DS3_v2 \
    --network-plugin azure \
    --network-policy calico \
    --enable-managed-identity \
    --enable-aad \
    --enable-azure-rbac \
    --enable-defender \
    --enable-cluster-autoscaler \
    --min-count 3 \
    --max-count 10 \
    --zones 1 2 3 \
    --uptime-sla \
    --generate-ssh-keys

GitOps Became the Standard Deployment Model

Flux and ArgoCD emerged as the GitOps tools of choice:

# Flux Kustomization for GitOps deployment
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: production-apps
  namespace: flux-system
spec:
  interval: 5m
  path: ./clusters/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: infra-repo
  validation: client
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: api-gateway
      namespace: production
  timeout: 3m
  postBuild:
    substitute:
      ENVIRONMENT: production
      CLUSTER_NAME: prod-aks-cluster
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
  name: infra-repo
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/company/infrastructure
  ref:
    branch: main
  secretRef:
    name: git-credentials

Service Mesh Adoption Grew

Istio and Linkerd provided the networking layer enterprises needed:

# Istio VirtualService for traffic management
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
  namespace: production
spec:
  hosts:
    - order-service
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: order-service
            subset: v2
          weight: 100
    - route:
        - destination:
            host: order-service
            subset: v1
          weight: 90
        - destination:
            host: order-service
            subset: v2
          weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
  namespace: production
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: UPGRADE
        http1MaxPendingRequests: 100
        http2MaxRequests: 1000
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

Security Became Priority One

Policy as code with OPA Gatekeeper became essential:

# Gatekeeper constraint template
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Missing required labels: %v", [missing])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
  parameters:
    labels: ["team", "environment", "cost-center"]

Observability Matured

The observability stack became standardized:

# Prometheus ServiceMonitor for app metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: order-service-metrics
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: order-service
  endpoints:
    - port: metrics
      interval: 15s
      path: /metrics
  namespaceSelector:
    matchNames:
      - production
---
# Grafana dashboard ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: "1"
data:
  order-service.json: |
    {
      "dashboard": {
        "title": "Order Service",
        "panels": [
          {
            "title": "Request Rate",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(http_requests_total{service=\"order-service\"}[5m])",
                "legendFormat": "{{method}} {{path}}"
              }
            ]
          },
          {
            "title": "Error Rate",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(http_requests_total{service=\"order-service\",status=~\"5..\"}[5m])",
                "legendFormat": "{{status}}"
              }
            ]
          }
        ]
      }
    }

Hard Lessons Learned

  1. Kubernetes is Not Magic: It doesn’t automatically make applications scalable or reliable
  2. Networking is Complex: CNI choices have long-term implications
  3. Security Requires Effort: Default configurations are not production-ready
  4. Cost Can Spiral: Right-sizing and autoscaling need constant attention
# Simple cost monitoring script
import kubernetes
from kubernetes import client, config
import json

config.load_kube_config()
v1 = client.CoreV1Api()

def analyze_resource_usage():
    namespaces = v1.list_namespace()
    report = []

    for ns in namespaces.items:
        ns_name = ns.metadata.name
        pods = v1.list_namespaced_pod(ns_name)

        total_cpu_request = 0
        total_memory_request = 0

        for pod in pods.items:
            for container in pod.spec.containers:
                if container.resources.requests:
                    cpu = container.resources.requests.get('cpu', '0')
                    memory = container.resources.requests.get('memory', '0')
                    total_cpu_request += parse_cpu(cpu)
                    total_memory_request += parse_memory(memory)

        report.append({
            'namespace': ns_name,
            'cpu_requests_cores': total_cpu_request,
            'memory_requests_gb': total_memory_request / (1024**3)
        })

    return report

What 2022 Holds

  • Kubernetes at the edge with K3s and similar
  • Better developer experience with platforms built on K8s
  • FinOps integration for cost management
  • Increased focus on supply chain security

Kubernetes in 2021 proved it’s the platform for cloud-native applications. The challenge now is making it accessible and manageable for everyone, not just infrastructure experts.

Resources

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.