Skip to content
Back to skills

Kubernetes Ops

ASecurity

Kubernetes operations, manifest generation, Helm charts, Karpenter autoscaling, GitOps (ArgoCD/Flux), network policies, pod security, and troubleshooting. Auto-activates on Kubernetes, K8s, kubectl, pod, deployment, service, ingress, helm, karpenter, eks, gke, aks, node, cluster, namespace, HPA, autoscaling.

  • 7 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 14, 2026
ai-agentsrustgonodekubernetesawsgitapifrontendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 14, 2026

npx -y skills add pfangueiro/claude-code-agents --skill kubernetes-ops --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kubernetes Ops?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kubernetes Ops
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pfangueiro-kubernetes-ops/badge)](https://www.skillsdirectory.com/skills/pfangueiro-kubernetes-ops)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kubernetes-ops
description: Kubernetes operations, manifest generation, Helm charts, Karpenter autoscaling, GitOps (ArgoCD/Flux), network policies, pod security, and troubleshooting. Auto-activates on Kubernetes, K8s, kubectl, pod, deployment, service, ingress, helm, karpenter, eks, gke, aks, node, cluster, namespace, HPA, autoscaling.
---

# Kubernetes Operations

## Overview

Production-ready Kubernetes patterns with security-hardened defaults. Covers manifest generation, autoscaling, GitOps, networking, and troubleshooting.

## Manifest Templates

### Deployment (Security-Hardened)

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: APP_NAME
  labels:
    app.kubernetes.io/name: APP_NAME
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/managed-by: helm
spec:
  # No `replicas` here on purpose — the HPA below owns this field. Pinning it while an
  # HPA is active flaps forever under GitOps; see the warning under the HPA template.
  # No HPA for this workload? Then add `replicas: 2` (omitting it defaults to 1).
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app.kubernetes.io/name: APP_NAME
  template:
    metadata:
      labels:
        app.kubernetes.io/name: APP_NAME
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: APP_NAME
      containers:
        - name: APP_NAME
          image: REGISTRY/APP_NAME:TAG
          ports:
            - containerPort: 8080
              protocol: TCP
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: APP_NAME
```

### PodDisruptionBudget

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: APP_NAME
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: APP_NAME
```

### NetworkPolicy (Default Deny + Allow Specific)

```yaml
# Default deny all ingress in namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
    - Ingress
---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-APP_NAME
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: APP_NAME
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: frontend
      ports:
        - port: 8080
          protocol: TCP
```

### HorizontalPodAutoscaler

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: APP_NAME
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: APP_NAME
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
```

**WARNING — pinned `replicas` + HPA + GitOps self-heal flap forever.** The Deployment above
composes safely with this HPA *only because it omits `spec.replicas`*. Put that field back and
the HPA scales the Deployment up while the GitOps engine reverts it to the pinned number, in a
loop, with **no error raised anywhere** — the only symptom is an oscillating replica count and a
storm of sync / `ScalingReplicaSet` events. Kubernetes says removing it is recommended, since
re-applying a pinned value while an HPA is active risks "thrashing or flapping behavior"
([HPA docs](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#migrating-deployments-and-statefulsets-to-horizontal-autoscaling)).

| Engine | Why it reverts the HPA | Fix |
|--------|------------------------|-----|
| Argo CD `selfHeal: true` | live replicas ≠ Git replicas ⇒ OutOfSync ⇒ self-heal patches it back | omit `replicas`; if a Helm chart forces it, use `ignoreDifferences` **plus** `RespectIgnoreDifferences=true` (see below) |
| Argo CD `ServerSideApply=true` | applies with `--force-conflicts`, seizing `spec.replicas` from the HPA's field manager | omit `replicas` — force-conflicts overrides field ownership |
| Flux `Kustomization` | every `interval` re-applies each field that diverges from Git; Flux has **no** field-level ignore | omit `replicas` (the [Flux FAQ](https://fluxcd.io/flux/faq/) requires it) |

Removing `replicas` costs a one-time dip to 1 pod on first apply (the API default) before the HPA
scales up to `minReplicas`. Avoid the dip by dropping the field from the live object first —
`kubectl apply edit-last-applied deployment/APP_NAME`, delete `spec.replicas`, then commit. The
same rule covers anything a controller owns: never pin in Git a field HPA, KEDA, or VPA writes.

## Karpenter (AWS Node Autoscaling)

Karpenter replaces Cluster Autoscaler with workload-aware, faster node provisioning.

### NodePool

```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        managed-by: karpenter
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["5"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h
  limits:
    cpu: "100"
    memory: 400Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
```

### EC2NodeClass

```yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiSelectorTerms:
    - alias: al2023@latest
  role: KarpenterNodeRole-CLUSTER_NAME
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: CLUSTER_NAME
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: CLUSTER_NAME
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 50Gi
        volumeType: gp3
        encrypted: true
```

### Autoscaling Tiers

| Tier | Tool | What It Scales | When to Use |
|------|------|---------------|-------------|
| 1 | HPA | Pod replicas | CPU/memory-based, always start here |
| 2 | VPA | Pod resource requests | Right-sizing, don't combine with HPA on same metric |
| 3 | Karpenter | Nodes | Workload-aware node provisioning, replaces Cluster Autoscaler |
| 4 | KEDA | Pod replicas | Event-driven (queue depth, Kafka lag, custom metrics) |

## GitOps Patterns

### ArgoCD Application

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: APP_NAME
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/ORG/REPO.git
    targetRevision: main
    path: k8s/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: APP_NAME
  # Belt-and-braces for charts that always render `replicas` and cannot omit it.
  # Needs BOTH of these: ignoreDifferences alone only silences the diff — Argo still
  # applies the desired state as-is during a sync, so replicas snap back on the next
  # commit or manual sync. RespectIgnoreDifferences pre-patches it out of the apply.
  # `name:` is REQUIRED here, not optional tidiness. Omit it and the rule matches EVERY
  # Deployment in this Application, so replica drift goes both undetected and unhealed
  # across all of them while the UI still reports Synced.
  ignoreDifferences:
    - group: apps
      kind: Deployment
      name: APP_NAME
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true
      - RespectIgnoreDifferences=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
```

### Flux Kustomization

```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: APP_NAME
  namespace: flux-system
spec:
  interval: 5m
  path: ./k8s/overlays/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: APP_NAME
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: APP_NAME
      namespace: APP_NAME
```

Flux has no equivalent of Argo's `ignoreDifferences`: this Kustomization re-applies every field
that diverges from Git on each `interval`, so omitting `spec.replicas` from the Deployment is the
*only* way to let an HPA hold a replica count here. See the HPA warning above.

## Helm Chart Scaffold

```
mychart/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-staging.yaml
├── values-prod.yaml
├── templates/
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── pdb.yaml
│   ├── networkpolicy.yaml
│   ├── serviceaccount.yaml
│   └── configmap.yaml
└── .helmignore
```

Always: pin chart versions, validate with `helm lint` and `helm template` in CI, use values files per environment.

## Troubleshooting Decision Trees

### Pod Not Starting

```
Pod status?
├── Pending
│   ├── Check events: kubectl describe pod POD
│   ├── "Insufficient cpu/memory" → check resource requests, node capacity
│   ├── "no nodes match" → check nodeSelector, tolerations, affinity
│   └── "PVC not bound" → check storage class, PV availability
├── CrashLoopBackOff  (the container keeps exiting non-zero; kubelet backs off
│   │                  10s → 20s → 40s …, capped at 300s, and resets the timer
│   │                  once the container has run 10 min without a problem)
│   ├── Check logs: kubectl logs POD --previous
│   ├── OOMKilled → increase memory limits
│   ├── Application error → fix code, check config
│   └── Liveness or startup probe failing → kubelet killed the container; compare
│       probe config against actual startup time (readiness CANNOT cause this)
├── ImagePullBackOff
│   ├── Check image name and tag exist
│   ├── Check imagePullSecrets for private registries
│   └── Check network access to registry
└── Error / Unknown
    ├── Check events: kubectl describe pod POD
    └── Check node status: kubectl get nodes
```

**Which probe can restart a container** — only two of the three can, and readiness is not
one of them ([pod lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)):

| Probe | kubelet action on failure | Restarts? | What you observe |
|-------|---------------------------|-----------|------------------|
| liveness | Kills the container; the Pod's restart policy applies | Yes | RESTARTS climbing → CrashLoopBackOff while it keeps exiting |
| startup | Kills the container; runs before liveness/readiness begin | Yes | Same, but only during startup |
| readiness | Kills nothing — the EndpointSlice controller removes the Pod IP from every matching Service | **No** | `Running`, `READY 0/1`, `RESTARTS 0`, endpoints empty |

So `READY 0/1` with `RESTARTS 0` is a readiness failure, never CrashLoopBackOff — diagnose it
under "Service Not Reachable" below, not here. The backoff numbers above are the defaults; the
`ReduceDefaultCrashLoopBackOffDecay` (1s start, 60s cap) and `KubeletCrashLoopBackOffMax`
feature gates change them, so confirm against your cluster before quoting a timing to anyone.

### Service Not Reachable

```
1. Verify pod is Running: kubectl get pods -l app=APP_NAME
2. Verify service exists: kubectl get svc APP_NAME
3. Check endpoints: kubectl get endpoints APP_NAME
   └── Empty? Either the Service and Pod selectors don't match, or every Pod is
       failing its readiness probe (kubectl get pods → READY 0/1, RESTARTS 0)
4. Test from within cluster: kubectl run debug --rm -it --image=busybox -- wget -qO- http://APP_NAME:PORT
5. Check network policies: kubectl get networkpolicy -n NAMESPACE
6. Check ingress/load balancer: kubectl describe ingress APP_NAME
```

## Security Checklist

- [ ] Pods run as non-root (`runAsNonRoot: true`)
- [ ] Read-only root filesystem (`readOnlyRootFilesystem: true`)
- [ ] All capabilities dropped (`drop: ["ALL"]`)
- [ ] No privilege escalation (`allowPrivilegeEscalation: false`)
- [ ] Seccomp profile set (`seccompProfile: RuntimeDefault`)
- [ ] Resource requests AND limits set on every container
- [ ] NetworkPolicies enforce least-privilege network access
- [ ] PodDisruptionBudgets protect availability during disruptions
- [ ] ServiceAccounts use least-privilege RBAC
- [ ] Images from trusted registries only, scanned with Trivy
- [ ] Secrets mounted as volumes, not environment variables
- [ ] No `latest` image tag — use immutable digests or semantic versions

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.