K3s: La Guía Completa para Cada Caso de Uso

K3s: La Guía Completa para Cada Caso de Uso

Domina K3s desde un nodo en desarrollo hasta producción multi-nodo: instalación, red, almacenamiento, GitOps, observabilidad, CI/CD, despliegues en edge y mantenimiento.

Por Omar Flores

Kubernetes es el estándar para orquestación de contenedores a escala, pero su peso operativo — etcd, el servidor de API, el controller manager, el scheduler, el cloud-controller, el plugin CNI — lo hace costoso de ejecutar por debajo de la escala empresarial. Un clúster completo de Kubernetes en un VPS de $10 es como operar una logística de distribución para un solo paquete.

K3s resuelve esto reduciendo Kubernetes a su superficie de API esencial manteniendo compatibilidad completa. Se distribuye como un binario único de menos de 100MB, reemplaza etcd con SQLite para despliegues de un solo nodo, incluye Flannel como CNI, Traefik como controlador de ingress y local-path-provisioner para almacenamiento. Obtienes un clúster de Kubernetes listo para producción que arranca en menos de 30 segundos en una máquina con 512MB de RAM.

Esta guía cubre cada caso de uso significativo: desarrollo local, producción en VPS único, clústeres HA multi-nodo, Raspberry Pi y dispositivos edge, configuraciones homelab, pipelines GitOps, stacks de observabilidad y operaciones del día dos. Cada sección es autocontenida — lee las que correspondan a tu situación.


Cómo Difiere K3s de K8s

Entender qué eliminó y qué reemplazó K3s te ayuda a razonar sobre sus limitaciones y fortalezas antes de comprometerte con él.

Eliminado de Kubernetes upstream:

  • Código de proveedor cloud integrado (controladores específicos de AWS, GCP, Azure)
  • Características en fase Alpha y APIs deprecadas
  • La mayoría de plugins y add-ons no esenciales

Reemplazado con alternativas más ligeras:

  • etcd → SQLite (nodo único) o etcd integrado (clúster HA)
  • kube-proxy → reemplazado por host-gw / VXLAN de Flannel
  • CoreDNS → incluido, misma versión
  • Ingress → Traefik v2 incluido por defecto
  • Almacenamiento → local-path-provisioner incluido

Lo que permanece idéntico a K8s upstream:

  • La API de Kubernetes — cada comando kubectl, cada manifiesto, cada CRD
  • El modelo de programación de pods
  • RBAC, secrets, configmaps, services, ingress
  • Compatibilidad con charts de Helm
  • Todos los tipos de carga de trabajo estándar (Deployment, StatefulSet, DaemonSet, Job, CronJob)

La implicación práctica: cualquier carga de trabajo que corre en Kubernetes upstream corre en K3s sin modificación. La diferencia está en la capa de infraestructura — cómo se ejecuta y gestiona el clúster en sí.


Caso 1: Desarrollo Local

El caso de uso más inmediato de K3s es reemplazar Docker Compose o Minikube para desarrollo local. K3s corre en Linux nativamente y en macOS/Windows via Multipass o Lima.

Linux (Nativo)

# instalar K3s como clúster de un solo nodo
curl -sfL https://get.k3s.io | sh -

# el script de instalación inicia K3s como servicio systemd
sudo systemctl status k3s

# verificar el nodo
sudo kubectl get nodes

# copiar kubeconfig para uso local
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config

macOS / Windows (via Lima)

Lima ejecuta una VM Linux con compartición automática de archivos y reenvío de puertos:

# instalar Lima
brew install lima

# crear instancia K3s
limactl start --name k3s template://k3s

# usar kubectl de K3s desde Lima
limactl shell k3s kubectl get nodes

# o configurar kubectl local para usarlo
limactl shell k3s -- cat /etc/rancher/k3s/k3s.yaml \
  | sed "s/127.0.0.1/$(limactl list k3s --format '{{.IP}}')/g" \
  > ~/.kube/k3s-lima.yaml
export KUBECONFIG=~/.kube/k3s-lima.yaml
kubectl get nodes

Flujo de Trabajo de Desarrollo

# construir y cargar imagen en K3s sin registro
docker build -t myapp:dev .
# K3s usa containerd, no Docker — importar directamente
docker save myapp:dev | sudo k3s ctr images import -

# o usar un registro local
docker run -d -p 5000:5000 --name registry registry:2

docker tag myapp:dev localhost:5000/myapp:dev
docker push localhost:5000/myapp:dev

# configurar K3s para confiar en el registro local
sudo tee /etc/rancher/k3s/registries.yaml << 'EOF'
mirrors:
  "localhost:5000":
    endpoint:
      - "http://localhost:5000"
EOF

sudo systemctl restart k3s

Caso 2: Producción en Nodo Único (VPS)

Un solo nodo K3s en un VPS de $10–20/mes maneja una cantidad sorprendente de carga de producción: una API Go con PostgreSQL, caché Redis, workers en segundo plano y Traefik manejando HTTPS — todo en 2 vCPUs y 4GB de RAM.

Requisitos del Servidor

Carga de trabajoMínimoRecomendado
Sistema K3s + Traefik512MB RAM, 1 vCPU1GB RAM, 1 vCPU
3–5 servicios pequeños2GB RAM total, 2 vCPU4GB RAM, 2 vCPU
PostgreSQL + Redis+1GB RAM+2GB RAM
Stack de monitoreo+512MB RAM+1GB RAM

Instalando K3s en un VPS

# como root en el VPS
# deshabilitar Traefik si quieres instalarlo manualmente con config personalizada
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik" sh -

# o mantener Traefik incluido (válido para la mayoría de los casos)
curl -sfL https://get.k3s.io | sh -

# obtener el token del nodo (necesario para añadir nodos agente después)
sudo cat /var/lib/rancher/k3s/server/node-token

Desplegando una Aplicación en Producción

Un conjunto completo de manifiestos para una API Go con PostgreSQL:

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: myapp
# postgres.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: myapp
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: local-path     # clase de almacenamiento por defecto de K3s
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: myapp
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:17-alpine
          env:
            - name: POSTGRES_DB
              value: myapp
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "$(POSTGRES_USER)"]
            initialDelaySeconds: 5
            periodSeconds: 5
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: myapp
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
# api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: yourregistry/myapp-api:2.1.0
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: api-secret
                  key: database-url
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: myapp
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080
# ingress.yaml — IngressRoute de Traefik
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: api-ingress
  namespace: myapp
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`api.tudominio.com`)
      kind: Rule
      services:
        - name: api
          port: 80
  tls:
    certResolver: letsencrypt

Gestión de Secretos

# crear secretos de forma imperativa
kubectl create secret generic postgres-secret \
  --namespace myapp \
  --from-literal=username=myapp \
  --from-literal=password=$(openssl rand -base64 32)

kubectl create secret generic api-secret \
  --namespace myapp \
  --from-literal=database-url="postgres://myapp:password@postgres:5432/myapp?sslmode=disable"

Caso 3: Clúster HA Multi-Nodo

Para cargas de trabajo de producción que necesitan alta disponibilidad, K3s soporta una configuración multi-servidor con etcd integrado. La configuración HA mínima es tres nodos servidor (para el quórum de etcd).

Arquitectura

                    ┌──────────────────────────────┐
                    │   Balanceador de carga (nginx) │
                    │      TCP pass-through :6443    │
                    └──────┬──────┬──────┬───────────┘
                           │      │      │
                    ┌──────▼──┐ ┌──▼─────▼┐ ┌──────────┐
                    │Server 1 │ │Server 2 │ │ Server 3 │
                    │ (etcd)  │ │ (etcd)  │ │  (etcd)  │
                    └─────────┘ └─────────┘ └──────────┘

              ┌────────────┼────────────┐
       ┌──────▼──┐  ┌──────▼──┐  ┌──────▼──┐
       │ Agent 1 │  │ Agent 2 │  │ Agent 3 │
       │(worker) │  │(worker) │  │(worker) │
       └─────────┘  └─────────┘  └──────────┘

Configurando el Clúster HA

# en Server 1 — inicializar el clúster con etcd integrado
curl -sfL https://get.k3s.io | sh -s - server \
  --cluster-init \
  --tls-san TU_LB_IP \
  --tls-san server1.internal \
  --disable traefik \
  --node-taint CriticalAddonsOnly=true:NoExecute

# obtener el token del clúster
sudo cat /var/lib/rancher/k3s/server/node-token

# en Server 2 y Server 3 — unirse al clúster
curl -sfL https://get.k3s.io | sh -s - server \
  --server https://SERVER1_IP:6443 \
  --token K10abc...::server:xyz... \
  --tls-san TU_LB_IP \
  --disable traefik \
  --node-taint CriticalAddonsOnly=true:NoExecute

# en cada nodo Agent
curl -sfL https://get.k3s.io | K3S_URL=https://TU_LB_IP:6443 \
  K3S_TOKEN=K10abc...::server:xyz... sh -

El --node-taint CriticalAddonsOnly=true:NoExecute en los nodos servidor evita que las cargas de trabajo sean programadas en los nodos del plano de control. El --tls-san TU_LB_IP añade la IP del balanceador al certificado TLS.

Configuración del Balanceador de Carga (nginx)

# /etc/nginx/nginx.conf
stream {
    upstream k3s_servers {
        server server1.internal:6443;
        server server2.internal:6443;
        server server3.internal:6443;
    }

    server {
        listen 6443;
        proxy_pass k3s_servers;
        proxy_timeout 10s;
        proxy_connect_timeout 5s;
    }
}

Caso 4: Raspberry Pi y Dispositivos Edge

K3s fue diseñado con ARM en mente. Corre en Raspberry Pi 4 (4GB), Raspberry Pi 5 y otras computadoras de placa única ARM.

Configuración de Raspberry Pi

# en Raspberry Pi OS (64-bit recomendado)
# habilitar cgroups — requerido para el runtime de contenedores
echo "cgroup_memory=1 cgroup_enable=memory" | sudo tee -a /boot/cmdline.txt
sudo reboot

# instalar K3s (detecta ARM64 automáticamente)
curl -sfL https://get.k3s.io | sh -

# verificar
sudo kubectl get nodes

Consideraciones específicas de ARM:

Usa imágenes de contenedor multi-arch (--platform linux/arm64). Las imágenes de solo amd64 fallarán con exec format error.

# construir imagen multi-arch con Docker Buildx
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag yourregistry/myapp:2.1.0 \
  --push .

Mueve el directorio de datos de K3s a un SSD USB para mejor rendimiento:

sudo mkdir -p /mnt/ssd
sudo mount /dev/sda1 /mnt/ssd
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--data-dir /mnt/ssd/k3s" sh -

Despliegues Edge sin Conexión

# en una máquina con internet, descargar artefactos K3s
VERSION=v1.31.0+k3s1
wget https://github.com/k3s-io/k3s/releases/download/$VERSION/k3s-arm64
wget https://github.com/k3s-io/k3s/releases/download/$VERSION/k3s-airgap-images-arm64.tar.zst

# transferir al dispositivo edge
scp k3s-arm64 pi@edgedispositivo:/usr/local/bin/k3s
scp k3s-airgap-images-arm64.tar.zst pi@edgedispositivo:/var/lib/rancher/k3s/agent/images/

# en el dispositivo edge
chmod +x /usr/local/bin/k3s
INSTALL_K3S_SKIP_DOWNLOAD=true \
INSTALL_K3S_VERSION=$VERSION \
./install.sh

# pre-cargar imágenes de la aplicación
docker save yourregistry/myapp:2.1.0 > myapp.tar
sudo k3s ctr images import myapp.tar

Caso 5: Homelab

Un clúster K3s en hardware viejo o un grupo de mini-PCs es una excelente forma de aprender patrones de Kubernetes de producción sin costos de nube.

MetalLB: LoadBalancer en Bare Metal

En bare metal, los servicios LoadBalancer se quedan en estado <Pending> para siempre sin una herramienta que los maneje. MetalLB asigna IPs reales desde un pool que defines.

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

kubectl wait --namespace metallb-system \
  --for=condition=ready pod \
  --selector=app=metallb \
  --timeout=90s
# metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: homelab-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.200-192.168.1.220   # reserva estas IPs en tu router
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: homelab-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - homelab-pool

cert-manager para HTTPS

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml

Para CA autofirmada (servicios internos):

# cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: homelab-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: homelab-ca
  secretName: homelab-ca-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: homelab-ca-issuer
spec:
  ca:
    secretName: homelab-ca-secret

Longhorn: Almacenamiento de Bloques Distribuido

El local-path incluido crea volúmenes locales al nodo — si el nodo muere, los datos se pierden. Longhorn provee almacenamiento de bloques replicado para clústeres multi-nodo.

kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.8.0/deploy/prerequisite/longhorn-iscsi-installation.yaml
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.8.0/deploy/longhorn.yaml

# establecer Longhorn como clase de almacenamiento por defecto
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass longhorn -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Caso 6: CI/CD — Desplegando en K3s desde GitHub Actions

Configurando el Acceso

# deploy-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: github-deploy
  namespace: myapp
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deploy-role
  namespace: myapp
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "patch", "list"]
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: github-deploy-binding
  namespace: myapp
subjects:
  - kind: ServiceAccount
    name: github-deploy
    namespace: myapp
roleRef:
  kind: Role
  name: deploy-role
  apiGroup: rbac.authorization.k8s.io

El Pipeline de Despliegue

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}/api

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Iniciar sesión en GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Construir y publicar
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Desplegar en K3s
        env:
          KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_B64 }}
        run: |
          echo "$KUBECONFIG_DATA" | base64 -d > /tmp/kubeconfig
          export KUBECONFIG=/tmp/kubeconfig

          kubectl set image deployment/api \
            api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            --namespace myapp

          kubectl rollout status deployment/api \
            --namespace myapp \
            --timeout=5m

Caso 7: GitOps con Flux

GitOps invierte el modelo CI/CD de push: en lugar de que CI empuje cambios al clúster, un controlador en el clúster jala desde un repositorio Git y aplica los cambios. El repositorio Git se convierte en la fuente única de verdad del estado del clúster.

Instalando Flux

# instalar CLI de Flux
curl -s https://fluxcd.io/install.sh | sudo bash

# bootstrap Flux en K3s
flux bootstrap github \
  --owner=tuorg \
  --repository=k3s-gitops \
  --branch=main \
  --path=clusters/production \
  --personal

GitRepository y Kustomization

# clusters/production/myapp/source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: myapp
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/tuorg/myapp
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: myapp
  namespace: flux-system
spec:
  interval: 5m
  path: ./k8s/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: myapp
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: api
      namespace: myapp

Automatización de Imágenes

# image-policy.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: myapp-api
  namespace: flux-system
spec:
  image: ghcr.io/tuorg/myapp/api
  interval: 5m
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: myapp-api
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: myapp-api
  policy:
    semver:
      range: '>=2.0.0 <3.0.0'
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: myapp
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: myapp
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxbot@tuorg.com
        name: Flux Bot
      messageTemplate: 'chore: actualizar {{range .Updated.Images}}{{println .}}{{end}}'
    push:
      branch: main
  update:
    path: ./k8s/production
    strategy: Setters

En tu manifiesto de Deployment, marca el campo de imagen para automatización:

containers:
  - name: api
    image: ghcr.io/tuorg/myapp/api:2.1.0 # {"$imagepolicy": "flux-system:myapp-api"}

Caso 8: Stack de Observabilidad

kube-prometheus-stack (Helm)

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword=$(openssl rand -base64 24) \
  --set prometheus.prometheusSpec.retention=7d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=local-path \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=10Gi

Exponiendo Grafana con Traefik

# grafana-ingress.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: grafana
  namespace: monitoring
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`grafana.tudominio.com`)
      kind: Rule
      services:
        - name: monitoring-grafana
          port: 80
  tls:
    certResolver: letsencrypt

ServiceMonitor para Métricas Personalizadas

# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp-api
  namespace: myapp
  labels:
    release: monitoring
spec:
  selector:
    matchLabels:
      app: api
  endpoints:
    - port: http
      path: /metrics
      interval: 30s

Loki para Logs

helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  --namespace monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.storageClassName=local-path \
  --set loki.persistence.size=10Gi

Caso 9: Red en Profundidad

Reemplazando Flannel con Cilium

curl -sfL https://get.k3s.io | sh -s - \
  --flannel-backend=none \
  --disable-network-policy \
  --disable traefik

helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium \
  --namespace kube-system \
  --set operator.replicas=1 \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=$(hostname -I | awk '{print $1}') \
  --set k8sServicePort=6443

Middlewares de Traefik

# middlewares.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: rate-limit
  namespace: myapp
spec:
  rateLimit:
    average: 100
    burst: 50
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: security-headers
  namespace: myapp
spec:
  headers:
    stsSeconds: 31536000
    stsIncludeSubdomains: true
    contentTypeNosniff: true
    frameDeny: true
    xssProtection: "1; mode=block"
    referrerPolicy: "strict-origin-when-cross-origin"

Operaciones del Día Dos

Actualizando K3s

# instalar el controlador de actualización del sistema
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
# upgrade-plan.yaml
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
  name: k3s-server
  namespace: system-upgrade
spec:
  concurrency: 1
  cordon: true
  nodeSelector:
    matchExpressions:
      - key: node-role.kubernetes.io/control-plane
        operator: In
        values: ["true"]
  serviceAccountName: system-upgrade
  upgrade:
    image: rancher/k3s-upgrade
  channel: https://update.k3s.io/v1-release/channels/stable
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
  name: k3s-agent
  namespace: system-upgrade
spec:
  concurrency: 2
  cordon: true
  nodeSelector:
    matchExpressions:
      - key: node-role.kubernetes.io/control-plane
        operator: DoesNotExist
  serviceAccountName: system-upgrade
  upgrade:
    image: rancher/k3s-upgrade
  prepare:
    image: rancher/k3s-upgrade
    args: ["prepare", "k3s-server"]
  channel: https://update.k3s.io/v1-release/channels/stable

Respaldo y Restauración

# respaldo de SQLite (nodo único)
sudo systemctl stop k3s
sudo cp /var/lib/rancher/k3s/server/db/state.db /backup/k3s-state-$(date +%Y%m%d).db
sudo systemctl start k3s

# respaldo automático con timer de systemd
sudo tee /etc/systemd/system/k3s-backup.service << 'EOF'
[Unit]
Description=Respaldo de SQLite de K3s

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'cp /var/lib/rancher/k3s/server/db/state.db /backup/k3s-$(date +%Y%m%d-%H%M).db && find /backup -name "k3s-*.db" -mtime +7 -delete'
EOF

sudo systemctl enable --now k3s-backup.timer

# snapshot de etcd (clúster HA)
sudo k3s etcd-snapshot save --name pre-upgrade-snapshot

# restaurar
sudo k3s server \
  --cluster-reset \
  --cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/pre-upgrade-snapshot

Mantenimiento de Nodos

# drenar un nodo
kubectl drain node-name \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --grace-period=60

# realizar mantenimiento (actualizaciones de SO, trabajo de hardware)

# descordonar después del mantenimiento
kubectl uncordon node-name

# desinstalar el agente K3s del nodo
/usr/local/bin/k3s-agent-uninstall.sh

Cuotas de Recursos por Namespace

# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: myapp-quota
  namespace: myapp
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 4Gi
    limits.cpu: "8"
    limits.memory: 8Gi
    pods: "20"
    persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: myapp-limits
  namespace: myapp
spec:
  limits:
    - type: Container
      default:
        cpu: "500m"
        memory: "256Mi"
      defaultRequest:
        cpu: "100m"
        memory: "128Mi"

Eligiendo la Configuración Correcta de K3s

DecisiónNodo único / devProducción HAHomelabEdge
Almacenamientolocal-pathLonghornLonghornlocal-path en SSD
CNIFlannelFlannel o CiliumFlannelFlannel
IngressTraefik (bundled)Traefik personalizadoTraefik + MetalLBTraefik
DB del clústerSQLiteetcd integradoSQLite o etcdSQLite
Nodos servidor13+1–31

K3s escala desde una sola Raspberry Pi hasta un clúster de cincuenta nodos bare metal, con la misma API de kubectl en cada configuración. La inversión en aprenderlo — los manifiestos, los charts de Helm, los patrones GitOps — se transfiere a cada destino de despliegue sin modificación.

K3s no es un Kubernetes simplificado. Es Kubernetes con el peso operativo eliminado — la misma API, la misma compatibilidad, el mismo ecosistema, corriendo en hardware que el Kubernetes completo no puede justificar.

Tags

#kubernetes #docker #devops #tutorial #guide #best-practices #backend