Qué vas a conseguir
Decidir si una aplicación interna debe pasar de Docker Compose a Kubernetes y, si pasa, desplegarla bien en un clúster gestionado: varias réplicas, configuración fuera de la imagen, recursos acotados, actualizaciones sin corte y la vuelta atrás en un comando. Y leer el problema más típico, un pod que se reinicia sin parar, en cinco minutos.
Antes de empezar
- La guía de Docker del Nivel 3: imágenes, registro y Compose.
- La imagen de la aplicación en un registro privado (aquí
acrcontoso.azurecr.io/inventario), con etiquetas de versión, nunca sololatest. kubectly la CLI del proveedor (azoaws), con acceso por tu identidad de Entra ID o IAM Identity Center.- Un responsable del clúster en el equipo: es una plataforma que hay que mantener.
1. ¿Compensa el salto?
| Situación | Recomendación |
|---|---|
| Una o dos herramientas internas, un servidor, se toleran unos minutos de corte al actualizar | Se queda en Compose. Copias, monitorización y un docker compose pull && up -d en ventana |
| Varios servicios, varios equipos que despliegan, se necesita alta disponibilidad | Kubernetes gestionado (AKS, EKS) |
| Muchas aplicaciones, requisitos de seguridad fuertes, se quiere un portal para desarrolladores | Red Hat OpenShift o similar, con un equipo de plataforma |
| Una sola web o API sin estado, sin ganas de gestionar nodos | Un servicio de contenedores sin clúster (Azure Container Apps, AWS App Runner o ECS con Fargate) |
Lo que da Kubernetes: réplicas que se reponen solas, actualizaciones progresivas y configuración declarativa en Git. Lo que cuesta: actualizar el clúster cada pocos meses, aprender otra herramienta y vigilar más piezas.
2. El clúster gestionado
En AKS, lo que importa al crear el clúster, mejor desde la guía de Terraform que a mano:
--tier standard(SLA del plano de control), nodos en tres zonas de disponibilidad, red Azure CNI Overlay y--attach-acr acrcontosopara leer el registro sin secretos.- Integración con Entra ID y Azure RBAC para Kubernetes, y
--disable-local-accounts: nadie entra con un certificado de administrador compartido. - La entrada HTTP: el complemento de enrutamiento de aplicaciones con Gateway API (
--enable-gateway-api --enable-app-routing-istio). El proyecto ingress-nginx se retiró en marzo de 2026 y Microsoft da parches para su versión gestionada hasta noviembre de 2026; un clúster nuevo empieza ya con Gateway API, el sucesor del recurso Ingress.
En EKS el equivalente es un clúster con nodos gestionados en tres zonas, acceso con EKS access entries ligadas a IAM Identity Center y el AWS Load Balancer Controller para la entrada.
3. La herramienta interna: Deployment, Service y Gateway
Un espacio de nombres por aplicación (herramientas) y cuatro objetos:
cat inventario.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: inventario namespace: herramientasspec: replicas: 2 strategy: rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } selector: matchLabels: { app: inventario } template: metadata: labels: { app: inventario } spec: containers: - name: inventario image: acrcontoso.azurecr.io/inventario:1.4.2 envFrom: - configMapRef: { name: inventario-config } - secretRef: { name: inventario-db } resources: requests: { cpu: 250m, memory: 256Mi } limits: { memory: 512Mi } readinessProbe: httpGet: { path: /healthz, port: 8080 }---apiVersion: v1kind: Servicemetadata: { name: inventario, namespace: herramientas }spec: selector: { app: inventario } ports: [{ port: 80, targetPort: 8080 }]---apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: { name: gw-herramientas, namespace: herramientas }spec: gatewayClassName: approuting-istio infrastructure: annotations: service.beta.kubernetes.io/azure-load-balancer-internal: "true" listeners: [{ name: http, port: 80, protocol: HTTP }]---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: { name: inventario, namespace: herramientas }spec: parentRefs: [{ name: gw-herramientas }] hostnames: ["inventario.intranet.contoso.com"] rules: [{ backendRefs: [{ name: inventario, port: 80 }] }]- Deployment: cuántas réplicas, qué imagen y cómo se actualiza.
maxUnavailable: 0significa que nunca baja de dos réplicas sanas durante una actualización. - Service: un nombre y una IP estables dentro del clúster delante de pods que vienen y van.
- Gateway + HTTPRoute: la entrada HTTP. La anotación crea un balanceador interno: la herramienta solo se ve desde la red de la empresa.
- readinessProbe: el pod no recibe tráfico hasta que
/healthzresponde.
4. Configuración, secretos y recursos
- ConfigMap para lo que no es secreto (
DB_HOST, nivel de log), en el mismo repositorio que el manifiesto. - Secret para la contraseña de la base de datos. Un Secret está en base64, no cifrado para quien pueda leerlo. Lo correcto es guardarlo en Key Vault o Secrets Manager y montarlo con el Secrets Store CSI Driver (en AKS, el complemento Azure Key Vault provider) y una identidad de carga de trabajo. Nunca en el repositorio.
- requests: lo que el planificador reserva en el nodo. Sin ellas, los pods acaban donde no caben.
- limits de memoria: si el contenedor los supera, se mata con OOMKilled. Para la CPU se suele poner solo la petición: un límite de CPU frena la aplicación sin avisar.
5. Actualizar y volver atrás
kubectl -n herramientas set image deployment/inventario inventario=acrcontoso.azurecr.io/inventario:1.5.0deployment.apps/inventario image updatedkubectl -n herramientas rollout status deployment/inventario --timeout=3mWaiting for deployment "inventario" rollout to finish: 1 out of 2 new replicas have been updated...error: timed out waiting for the conditionkubectl -n herramientas get podsNAME READY STATUS RESTARTS AGEinventario-5d8b7f6c49-kq7zt 1/1 Running 0 6dinventario-5d8b7f6c49-w2m9r 1/1 Running 0 6dinventario-7c9f8d6b5-x2k4p 0/1 CrashLoopBackOff 5 (40s ago) 4mkubectl -n herramientas rollout undo deployment/inventariodeployment.apps/inventario rolled backGracias a maxUnavailable: 0 y a la sonda de disponibilidad, los dos pods de la 1.4.2 siguieron atendiendo: los usuarios no notaron nada. rollout undo vuelve a la revisión anterior; rollout history las lista. En el día a día la imagen se cambia desde el pipeline o con GitOps (Argo CD, Flux).
6. El ticket: "el pod está en CrashLoopBackOff"
CrashLoopBackOff no es la causa: el contenedor arranca, muere y Kubernetes lo reintenta cada vez más tarde. La causa está en tres sitios:
kubectl -n herramientas describe pod inventario-7c9f8d6b5-x2k4p State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Exit Code: 1 Restart Count: 5Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning BackOff 15s (x18 over 4m) kubelet Back-off restarting failed container inventario in pod inventario-7c9f8d6b5-x2k4pkubectl -n herramientas logs inventario-7c9f8d6b5-x2k4p --previous2026-10-28T09:41:07Z INFO inventario 1.5.0 arrancando2026-10-28T09:41:07Z FATAL falta la variable DATABASE_URL; desde 1.5.0 sustituye a DB_HOST y DB_NAMEkubectl -n herramientas get events --sort-by=.lastTimestamp | tail -5describe: el motivo y el código de salida del último intento, y los eventos del pod.logs --previous: lo que escribió el contenedor anterior, el que murió. Sin--previousves el intento actual, que a menudo aún no ha escrito nada.get events: lo que pasa alrededor (nodo sin recursos, volumen que no monta, imagen que no baja).
| Lo que ves | Qué suele ser |
|---|---|
| Error, código 1 | La aplicación falla al arrancar: configuración, una variable, la base de datos. Lo dicen los logs |
| OOMKilled, código 137 | Superó el límite de memoria. Subir el límite o revisar la aplicación |
| CreateContainerConfigError | Falta el ConfigMap o el Secret que referencia el pod |
| ImagePullBackOff | Otra familia: la etiqueta no existe o el clúster no puede leer el registro |
Aquí la versión 1.5.0 cambió el nombre de una variable y nadie actualizó el ConfigMap. Se añade DATABASE_URL (en el Secret, porque lleva la contraseña), se revisa en pruebas y se repite la actualización.
7. Lo que revisas cada mes
- Versión del clúster frente al calendario de soporte de AKS o EKS: la siguiente actualización tiene fecha, primero en pruebas.
- Pods con reinicios en los últimos 30 días y pods sin
requestso sin sondas. - Imágenes con vulnerabilidades críticas en el registro (Defender for Containers, Amazon Inspector).
Siguientes pasos
- La guía de Red Hat OpenShift si lo que se necesita es una plataforma completa con portal, compilaciones y políticas.
- La guía de Terraform para que el clúster, el registro y la red salgan de código revisado.
- La guía de Azure Monitor y Log Analytics para Container Insights y alertas sobre reinicios, y la de Grafana para cuadros de mando con Prometheus.
- La guía de Amazon AWS si el clúster va a ser EKS.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
What you'll get
Deciding whether an internal application should move from Docker Compose to Kubernetes and, if it does, deploying it properly on a managed cluster: several replicas, configuration outside the image, bounded resources, updates without downtime and rollback in one command. And reading the most typical problem, a pod that keeps restarting, in five minutes.
Before you start
- The Level 3 Docker guide: images, registry and Compose.
- The application image in a private registry (
acrcontoso.azurecr.io/inventoryhere), with version tags, never justlatest. kubectland the provider CLI (azoraws), with access through your Entra ID or IAM Identity Center identity.- A cluster owner on the team: it is a platform that needs maintaining.
1. Is the jump worth it?
| Situation | Recommendation |
|---|---|
| One or two internal tools, one server, a few minutes of downtime when updating is acceptable | Stay on Compose. Backups, monitoring and a docker compose pull && up -d in a window |
| Several services, several teams deploying, high availability needed | Managed Kubernetes (AKS, EKS) |
| Many applications, strong security requirements, a developer portal wanted | Red Hat OpenShift or similar, with a platform team |
| A single stateless website or API, no appetite for managing nodes | A container service without a cluster (Azure Container Apps, AWS App Runner or ECS with Fargate) |
What Kubernetes gives you: self-healing replicas, rolling updates and declarative configuration in Git. What it costs: upgrading the cluster every few months, learning another tool and watching more moving parts.
2. The managed cluster
On AKS, what matters when creating the cluster, better from the Terraform guide than by hand:
--tier standard(control plane SLA), nodes across three availability zones, Azure CNI Overlay networking and--attach-acr acrcontosoto read the registry without secrets.- Entra ID integration and Azure RBAC for Kubernetes, and
--disable-local-accounts: nobody gets in with a shared admin certificate. - HTTP ingress: the application routing add-on with Gateway API (
--enable-gateway-api --enable-app-routing-istio). The ingress-nginx project was retired in March 2026 and Microsoft provides patches for its managed version until November 2026; a new cluster starts straight away with Gateway API, the successor to the Ingress resource.
On EKS the equivalent is a cluster with managed node groups across three zones, access through EKS access entries tied to IAM Identity Center, and the AWS Load Balancer Controller for ingress.
3. The internal tool: Deployment, Service and Gateway
One namespace per application (tools) and four objects:
cat inventory.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: inventory namespace: toolsspec: replicas: 2 strategy: rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } selector: matchLabels: { app: inventory } template: metadata: labels: { app: inventory } spec: containers: - name: inventory image: acrcontoso.azurecr.io/inventory:1.4.2 envFrom: - configMapRef: { name: inventory-config } - secretRef: { name: inventory-db } resources: requests: { cpu: 250m, memory: 256Mi } limits: { memory: 512Mi } readinessProbe: httpGet: { path: /healthz, port: 8080 }---apiVersion: v1kind: Servicemetadata: { name: inventory, namespace: tools }spec: selector: { app: inventory } ports: [{ port: 80, targetPort: 8080 }]---apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: { name: gw-tools, namespace: tools }spec: gatewayClassName: approuting-istio infrastructure: annotations: service.beta.kubernetes.io/azure-load-balancer-internal: "true" listeners: [{ name: http, port: 80, protocol: HTTP }]---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: { name: inventory, namespace: tools }spec: parentRefs: [{ name: gw-tools }] hostnames: ["inventory.intranet.contoso.com"] rules: [{ backendRefs: [{ name: inventory, port: 80 }] }]- Deployment: how many replicas, which image and how it is updated.
maxUnavailable: 0means it never drops below two healthy replicas during an update. - Service: a stable name and IP inside the cluster in front of pods that come and go.
- Gateway + HTTPRoute: HTTP ingress. The annotation creates an internal load balancer: the tool is only visible from the company network.
- readinessProbe: the pod receives no traffic until
/healthzanswers.
4. Configuration, secrets and resources
- ConfigMap for what isn't secret (
DB_HOST, log level), in the same repository as the manifest. - Secret for the database password. A Secret is base64, not encrypted for anyone who can read it. The right way is to keep it in Key Vault or Secrets Manager and mount it with the Secrets Store CSI Driver (on AKS, the Azure Key Vault provider add-on) and a workload identity. Never in the repository.
- requests: what the scheduler reserves on the node. Without them, pods end up where they don't fit.
- Memory limits: if the container exceeds them, it is killed with OOMKilled. For CPU usually only the request is set: a CPU limit throttles the application silently.
5. Updating and rolling back
kubectl -n tools set image deployment/inventory inventory=acrcontoso.azurecr.io/inventory:1.5.0deployment.apps/inventory image updatedkubectl -n tools rollout status deployment/inventory --timeout=3mWaiting for deployment "inventory" rollout to finish: 1 out of 2 new replicas have been updated...error: timed out waiting for the conditionkubectl -n tools get podsNAME READY STATUS RESTARTS AGEinventory-5d8b7f6c49-kq7zt 1/1 Running 0 6dinventory-5d8b7f6c49-w2m9r 1/1 Running 0 6dinventory-7c9f8d6b5-x2k4p 0/1 CrashLoopBackOff 5 (40s ago) 4mkubectl -n tools rollout undo deployment/inventorydeployment.apps/inventory rolled backThanks to maxUnavailable: 0 and the readiness probe, the two 1.4.2 pods kept serving: users noticed nothing. rollout undo goes back to the previous revision; rollout history lists them. Day to day the image is changed from the pipeline or with GitOps (Argo CD, Flux).
6. The ticket: "the pod is in CrashLoopBackOff"
CrashLoopBackOff is not the cause: the container starts, dies and Kubernetes retries it later each time. The cause is in three places:
kubectl -n tools describe pod inventory-7c9f8d6b5-x2k4p State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Exit Code: 1 Restart Count: 5Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning BackOff 15s (x18 over 4m) kubelet Back-off restarting failed container inventory in pod inventory-7c9f8d6b5-x2k4pkubectl -n tools logs inventory-7c9f8d6b5-x2k4p --previous2026-10-28T09:41:07Z INFO inventory 1.5.0 starting2026-10-28T09:41:07Z FATAL missing variable DATABASE_URL; since 1.5.0 it replaces DB_HOST and DB_NAMEkubectl -n tools get events --sort-by=.lastTimestamp | tail -5describe: the reason and exit code of the last attempt, and the pod's events.logs --previous: what the previous container wrote, the one that died. Without--previousyou see the current attempt, which often hasn't written anything yet.get events: what is happening around it (node short of resources, volume that won't mount, image that won't pull).
| What you see | What it usually is |
|---|---|
| Error, code 1 | The application fails at startup: configuration, a variable, the database. The logs say which |
| OOMKilled, code 137 | It exceeded the memory limit. Raise the limit or look at the application |
| CreateContainerConfigError | The ConfigMap or Secret the pod references is missing |
| ImagePullBackOff | A different family: the tag doesn't exist or the cluster can't read the registry |
Here version 1.5.0 renamed a variable and nobody updated the ConfigMap. DATABASE_URL is added (in the Secret, as it carries the password), checked in test and the update repeated.
7. What you review each month
- Cluster version against the AKS or EKS support calendar: the next upgrade has a date, test first.
- Pods with restarts in the last 30 days and pods without
requestsor without probes. - Images with critical vulnerabilities in the registry (Defender for Containers, Amazon Inspector).
Next steps
- The Red Hat OpenShift guide if what you need is a complete platform with a portal, builds and policies.
- The Terraform guide so that the cluster, registry and network come out of reviewed code.
- The Azure Monitor y Log Analytics guide for Container Insights and alerts on restarts, and the Grafana guide for dashboards with Prometheus.
- The Amazon AWS guide if the cluster is going to be EKS.
Is something missing, or different in your environment? Get in touch and I'll fix it.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →