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
- Kubernetes is Not Magic: It doesn’t automatically make applications scalable or reliable
- Networking is Complex: CNI choices have long-term implications
- Security Requires Effort: Default configurations are not production-ready
- 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.