Universidad Autónoma de Occidente (UAO) Especialización en Inteligencia Artificial — Computación en Nube Estudiantes: Javier Henao Hernández & Jorge Enrique Gutiérrez Ramírez
Guía base: M3A3_Consigna_ComputacionNube.pdf
Fecha de ejecución: 10 de junio de 2026
Equipo: macOS Apple Silicon M1 (arm64), 8 GB RAM
Implementar un clúster de Kubernetes en Azure (AKS), desplegar una aplicación de clasificación de imágenes con deep learning (PyTorch), desplegar una aplicación de interés (AKS Store Demo) y demostrar los servicios de supervisión y monitoreo de Azure.
| Componente | Versión / Detalle |
|---|---|
| macOS | Apple Silicon M1 (arm64), 8 GB RAM |
| Azure CLI | 2.87.0 (Homebrew, /opt/homebrew) |
| kubectl (client) | v1.34.1 (Homebrew) |
| Kubernetes (AKS server) | v1.34.8 |
| Nodos AKS | 2 × Standard_D2s_v3 (2 vCPU, 8 GB RAM) |
| Región Azure | East US 2 |
| Suscripción | Azure subscription 1 (UAO) |
| Imagen clasificador | python:3.9-slim + PyTorch 2.8.0 (ConfigMap) |
| Modelo DL | ResNet-18 (ImageNet, torchvision) |
| App de interés | AKS Store Demo (Microsoft) |
| Monitoring | Container Insights + Log Analytics |
macOS M1 (host) — Terminal zsh
└── Azure CLI 2.87.0 + kubectl v1.34.1
└── Azure Kubernetes Service (AKS) — East US 2
└── Clúster: aks-m3a3 (rg-aks-m3a3)
├── Nodo 1: aks-nodepool1-41219197-vmss000000 (Standard_D2s_v3)
├── Nodo 2: aks-nodepool1-41219197-vmss000001 (Standard_D2s_v3)
│
├── Namespace: default
│ └── Deployment: kubermatic-dl-deployment (1 réplica)
│ └── Pod: python:3.9-slim + PyTorch ResNet-18
│ └── Service: LoadBalancer → IP: 20.161.161.155:80 → :5000
│
├── Namespace: tienda
│ ├── Deployment: store-front (Vue.js frontend)
│ │ └── Service: LoadBalancer → IP: 135.222.152.166:80
│ ├── Deployment: order-service
│ ├── Deployment: product-service
│ └── StatefulSet: rabbitmq-0 (cola de mensajes)
│
└── Monitoring: Container Insights + Log Analytics Workspace
brew update
brew install azure-cli
az version
# → "azure-cli": "2.87.0"
brew install kubectl
kubectl version --client
# → Client Version: v1.34.1az login
# → Se abre el navegador, iniciar sesión con cuenta UAO
# → Seleccionar suscripción: presionar Enter (default)
az account show --output table
# → Name: Azure subscription 1
# → TenantDisplayName: Universidad Autonoma de Occidente
# → State: Enabledaz provider register --namespace Microsoft.OperationalInsights
az provider register --namespace Microsoft.OperationsManagement
az provider register --namespace microsoft.insights
az provider register --namespace Microsoft.ContainerService
# Verificar que estén registrados
az provider show -n Microsoft.OperationalInsights --query registrationState -o tsv
# → Registered
az provider show -n Microsoft.OperationsManagement --query registrationState -o tsv
# → Registered
az provider show -n microsoft.insights --query registrationState -o tsv
# → RegisteredNOTA: Este paso solo es necesario la primera vez. Los proveedores quedan registrados permanentemente en la suscripción.
az group create --name rg-aks-m3a3 --location eastus2
# → "provisioningState": "Succeeded"az aks create \
--resource-group rg-aks-m3a3 \
--name aks-m3a3 \
--node-count 2 \
--node-vm-size Standard_D2s_v3 \
--generate-ssh-keys \
--enable-addons monitoring \
--tier free
# → Tarda ~5-10 minutos
# → "provisioningState": "Succeeded"IMPORTANTE: La suscripción Azure for Students de la UAO no permite
Standard_B2seneastus2. Se usóStandard_D2s_v3(2 vCPU, 8 GB RAM), que es la opción más pequeña disponible.
az aks get-credentials --resource-group rg-aks-m3a3 --name aks-m3a3
# → Merged "aks-m3a3" as current context in /Users/javher/.kube/config
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# aks-nodepool1-41219197-vmss000000 Ready <none> 3m29s v1.34.8
# aks-nodepool1-41219197-vmss000001 Ready <none> 3m22s v1.34.8
kubectl cluster-info
# Kubernetes control plane is running at https://aks-m3a3-rg-aks-m3a3-68d078-ezkxxj7m.hcp.eastus2.azmk8s.io:443
# CoreDNS is running at https://...
# Metrics-server is running at https://...- Ir a portal.azure.com
- Click en el ícono Cloud Shell (>_) en la barra superior
- Ejecutar:
az aks get-credentials --resource-group rg-aks-m3a3 --name aks-m3a3
kubectl get nodes
# → 2 nodos ReadyEvidencias: Capturas de terminal Mac y Cloud Shell mostrando 2 nodos Ready.
La guía original sugiere usar un clasificador basado en MXNet. Se encontraron los siguientes problemas y se aplicaron las soluciones correspondientes:
| Problema | Causa | Solución |
|---|---|---|
| Imagen Docker original solo amd64 | Incompatibilidad de arquitectura | Usar python:3.9-slim (multiarch) |
| MXNet falla en ARM | libarmpl_lp64_mp.so not found |
Refactorizar a PyTorch |
| ACR bloqueado | TasksOperationsNotAllowed en suscripciones académicas |
Inyectar código via ConfigMap |
| Pod en Pending | Falta de recursos en nodos pequeños | replicas: 1 y limitar resources |
mkdir -p ~/Desktop/devs/aks-m3a3/clasificador
cd ~/Desktop/devs/aks-m3a3/clasificadorcat > app.py << 'PYEOF'
import io
import json
import torch
import torchvision.transforms as transforms
from torchvision import models
from PIL import Image
from flask import Flask, request, jsonify
app = Flask(__name__)
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
model.eval()
import urllib.request
url = "https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt"
try:
response = urllib.request.urlopen(url)
categories = [s.strip().decode("utf-8") for s in response.readlines()]
except Exception:
categories = [f"class_{i}" for i in range(1000)]
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
@app.route("/predict", methods=["POST"])
def predict():
if "img" not in request.files:
return jsonify({"error": "No image provided. Use: curl -X POST -F img=@image.jpg URL/predict"}), 400
file = request.files["img"]
img_bytes = file.read()
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
tensor = transform(img).unsqueeze(0)
with torch.no_grad():
outputs = model(tensor)
probabilities = torch.nn.functional.softmax(outputs[0], dim=0)
top_prob, top_catid = torch.topk(probabilities, 1)
category = categories[top_catid[0].item()]
probability = top_prob[0].item()
result = f"The input picture is classified as [{category}], with probability {probability:.3f}"
print(result)
return result + "\n"
@app.route("/", methods=["GET"])
def health():
return "Image Classifier (PyTorch ResNet-18) is running!\n"
if __name__ == "__main__":
print("Starting Image Classifier on port 5000...")
app.run(host="0.0.0.0", port=5000)
PYEOFcat > requirements.txt << 'EOF'
flask==3.0.3
torch
torchvision
Pillow
EOF
cat > Dockerfile << 'EOF'
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
EOFEl archivo deployment.yaml contiene dos objetos Kubernetes: un ConfigMap con el código fuente (app.py y requirements.txt) y un Deployment que monta ese ConfigMap como volumen, instala las dependencias al arrancar y ejecuta la aplicación Flask.
# deployment.yaml (estructura resumida)
apiVersion: v1
kind: ConfigMap
metadata:
name: classifier-code
data:
app.py: |
# ... (código completo de app.py)
requirements.txt: |
flask==3.0.3
torch
torchvision
Pillow
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubermatic-dl-deployment
spec:
replicas: 1
selector:
matchLabels:
app: kubermatic-dl
template:
spec:
containers:
- name: kubermatic-dl
image: python:3.9-slim
command: ["/bin/bash", "-c"]
args:
- |
pip install --no-cache-dir -r /app/requirements.txt &&
python /app/app.py
ports:
- containerPort: 5000
volumeMounts:
- name: code-volume
mountPath: /app
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
volumes:
- name: code-volume
configMap:
name: classifier-codekubectl apply -f deployment.yaml
# → configmap/classifier-code created
# → deployment.apps/kubermatic-dl-deployment created
kubectl get pods -w
# → kubermatic-dl-deployment-79d9f5489d-srx95 1/1 Running 0 7s
# (Ctrl+C)
# Verificar logs (el pod descarga ~888 MB de PyTorch + modelo ResNet-18)
kubectl logs -f $(kubectl get pods -l app=kubermatic-dl -o jsonpath='{.items[0].metadata.name}')
# → Successfully installed ... torch-2.8.0 torchvision-0.23.0 ...
# → Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth"
# → Starting Image Classifier on port 5000...
# → Running on http://0.0.0.0:5000
# (Ctrl+C)kubectl expose deployment kubermatic-dl-deployment \
--type=LoadBalancer \
--port=80 \
--target-port=5000
# → service/kubermatic-dl-deployment exposed
kubectl get service kubermatic-dl-deployment --watch
# → kubermatic-dl-deployment LoadBalancer 10.0.246.208 20.161.161.155 80:30442/TCP
# (Ctrl+C)
export CLASSIFIER_IP=$(kubectl get service kubermatic-dl-deployment -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Classifier IP: $CLASSIFIER_IP"
# → Classifier IP: 20.161.161.155# Health check
curl http://$CLASSIFIER_IP/
# → Image Classifier (PyTorch ResNet-18) is running!
# Descargar imagen de prueba (debe ser un JPG real, >10 KB)
curl -L -o perro.jpg "https://cdn.pixabay.com/photo/2016/12/13/05/15/puppy-1903313_640.jpg"
ls -lh perro.jpg
# → 54K (imagen válida)
# Clasificar la imagen
curl -X POST -F "img=@perro.jpg" http://$CLASSIFIER_IP/predict
# → The input picture is classified as [Labrador retriever], with probability 0.223Evidencias: Terminal con resultado de clasificación + imagen de prueba utilizada.
Se seleccionó la aplicación oficial de Microsoft "AKS Store Demo" (Contoso Pet Store): una tienda de mascotas con arquitectura de microservicios compuesta por frontend Vue.js, servicio de pedidos, servicio de productos y RabbitMQ como cola de mensajes.
kubectl create namespace tienda
# → namespace/tienda created
kubectl apply -n tienda -f https://raw.githubusercontent.com/Azure-Samples/aks-store-demo/main/aks-store-quickstart.yaml
# → statefulset.apps/rabbitmq created
# → configmap/rabbitmq-enabled-plugins created
# → service/rabbitmq created
# → deployment.apps/order-service created
# → service/order-service created
# → deployment.apps/product-service created
# → service/product-service created
# → deployment.apps/store-front created
# → service/store-front created
kubectl get pods -n tienda -w
# → Esperar hasta que todos estén 1/1 Running
# → order-service-dffd4f9cf-v4ztz 1/1 Running 0 94s
# → product-service-c6b8587b-gjqtc 1/1 Running 0 94s
# → rabbitmq-0 1/1 Running 0 95s
# → store-front-845c99fb8b-lbm5h 1/1 Running 0 93s
# (Ctrl+C)kubectl get service -n tienda --watch
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# order-service ClusterIP 10.0.186.24 <none> 3000/TCP 30s
# product-service ClusterIP 10.0.169.65 <none> 3002/TCP 29s
# rabbitmq ClusterIP 10.0.255.207 <none> 5672/TCP,15672/TCP 31s
# store-front LoadBalancer 10.0.200.145 135.222.152.166 80:32042/TCP 28s
# (Ctrl+C)Abrir en Safari/Chrome: http://135.222.152.166
Se muestra la tienda "Contoso Pet Store" con 5 productos para mascotas (Contoso Catnip's Friend, Salty Sailor's Squeaky Squid, Mermaid's Mice Trio, Ocean Explorer's Puzzle Ball, Pirate Parrot Teaser Wand) con carrito de compras funcional.
Evidencias: Terminal con pods Running + servicio con EXTERNAL-IP + navegador con la tienda visible.
kubectl top nodes
# NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
# aks-nodepool1-41219197-vmss000000 252m 13% 2232Mi 38%
# aks-nodepool1-41219197-vmss000001 185m 9% 1627Mi 28%
kubectl top pods --all-namespaces
# NAMESPACE NAME CPU(cores) MEMORY(bytes)
# default kubermatic-dl-deployment-79d9f5489d-srx95 1m 531Mi
# kube-system ama-logs-c4m8b 11m 236Mi
# kube-system ama-logs-cmmbq 13m 239Mi
# ...
# tienda order-service-dffd4f9cf-v4ztz 2m 68Mi
# tienda product-service-c6b8587b-gjqtc 1m 2Mi
# tienda rabbitmq-0 4m 136Mi
# tienda store-front-845c99fb8b-lbm5h 1m 3Mifor i in $(seq 1 200); do
curl -s -X POST -F "img=@perro.jpg" http://$CLASSIFIER_IP/predict > /dev/null 2>&1 &
done
wait
echo "200 peticiones completadas"
# → 200 peticiones completadas
# Verificar pico de CPU tras la carga
kubectl top nodes
# NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
# aks-nodepool1-41219197-vmss000000 440m 23% 2470Mi 42%
# aks-nodepool1-41219197-vmss000001 184m 9% 1633Mi 28%El nodo 1 subió de 13% a 23% de CPU tras la prueba de carga.
- Portal de Azure → buscar clúster
aks-m3a3 - Menú lateral → Supervise → Insights
- Visualizar:
- Resumen del clúster: 2 nodos, ~31 pods total
- Gráficas de CPU y memoria por nodo (temporal)
- Pestaña "Containers" con cada contenedor
- Menú del clúster → Metrics (Métricas)
- Agregar:
- Metric:
CPU Usage Percentage, Aggregation:Avg - Metric:
Number of pods by phase, Aggregation:Avg
- Metric:
- Menú del clúster → Records (Registros / Logs)
- Consultas KQL:
Inventario de pods por namespace:
KubePodInventory
| summarize Pods = dcount(Name) by NamespaceResultado esperado: kube-system (~30), default (~9), tienda (~4)
Logs del clasificador (últimos 50):
ContainerLogV2
| where ContainerName has "kubermatic-dl"
| order by TimeGenerated desc
| take 50Evidencias: Capturas de Insights (resumen clúster, CPU, memoria), Métricas (gráficas), Logs (KubePodInventory, ContainerLogV2), y terminal con
kubectl topantes y después de la prueba de carga.
# Elimina clúster, nodos, IPs, Load Balancers, Log Analytics, TODO
az group delete --name rg-aks-m3a3 --yes --no-wait
# También eliminar el grupo de recursos de Log Analytics creado automáticamente
az group delete --name DefaultResourceGroup-EUS2 --yes --no-wait
# Verificar que se están eliminando
az group list --output table
# Esperar hasta que la tabla esté vacía (5-10 min)
# Limpiar contexto local de kubectl
kubectl config delete-context aks-m3a3
kubectl config delete-cluster aks-m3a3
kubectl config delete-user clusterUser_rg-aks-m3a3_aks-m3a3az aks stop --resource-group rg-aks-m3a3 --name aks-m3a3
# Los nodos se apagan, no consumen cómputo (pero el disco sigue cobrando)
# Para reactivar:
az aks start --resource-group rg-aks-m3a3 --name aks-m3a3#!/bin/bash
# recrear-aks.sh — Ejecutar ~30 min antes de la sustentación
set -e
# 1. Crear grupo de recursos y clúster (~10 min)
az group create --name rg-aks-m3a3 --location eastus2
az aks create \
--resource-group rg-aks-m3a3 \
--name aks-m3a3 \
--node-count 2 \
--node-vm-size Standard_D2s_v3 \
--generate-ssh-keys \
--enable-addons monitoring \
--tier free
# 2. Conectar kubectl
az aks get-credentials --resource-group rg-aks-m3a3 --name aks-m3a3
# 3. Desplegar clasificador (~5 min para pip install + modelo)
cd ~/Desktop/devs/aks-m3a3/clasificador
kubectl apply -f deployment.yaml
kubectl expose deployment kubermatic-dl-deployment \
--type=LoadBalancer --port=80 --target-port=5000
# 4. Desplegar tienda (~2 min)
kubectl create namespace tienda
kubectl apply -n tienda -f https://raw.githubusercontent.com/Azure-Samples/aks-store-demo/main/aks-store-quickstart.yaml
# 5. Esperar y mostrar IPs
echo "Esperando IPs externas..."
sleep 60
echo "=== Clasificador ==="
kubectl get service kubermatic-dl-deployment
echo "=== Tienda ==="
kubectl get service store-front -n tienda
echo "=== Nodos ==="
kubectl get nodes~/Desktop/devs/aks-m3a3/
├── clasificador/
│ ├── app.py ← Código del clasificador PyTorch
│ ├── requirements.txt ← Dependencias Python
│ ├── Dockerfile ← Para referencia (no se usa con ConfigMap)
│ └── deployment.yaml ← ConfigMap + Deployment (ARCHIVO CLAVE)
├── perro.jpg ← Imagen de prueba (54 KB)
├── recrear-aks.sh ← Script de recreación rápida
└── GUIA_PASO_A_PASO_M3A3.md ← Este documento
| # | Error / síntoma | Causa | Solución |
|---|---|---|---|
| 1 | MissingSubscriptionRegistration para Microsoft.OperationalInsights |
Proveedores no registrados en suscripción nueva | az provider register --namespace Microsoft.OperationalInsights (+ OperationsManagement, microsoft.insights) |
| 2 | Standard_B2s is not allowed |
Suscripción UAO no permite B-series en eastus2 | Usar Standard_D2s_v3 (2 vCPU, 8 GB) |
| 3 | UnidentifiedImageError en PIL |
Imagen descargada era HTML (redirect), no JPG real | Usar curl -L y verificar tamaño (>10 KB) |
| 4 | 404 en /predict |
Pod se reinició durante pip install tras error previo | Esperar a que Flask arranque (kubectl logs -f) |
| 5 | Selección de suscripción: Invalid selection |
Se intentó pegar el UUID en vez de presionar Enter | Presionar Enter para aceptar el default |
| Aspecto | Windows (PowerShell) | Mac M1 (Terminal zsh) |
|---|---|---|
| Azure CLI | Installer .msi | brew install azure-cli |
| kubectl | az aks install-cli |
brew install kubectl |
| curl | curl.exe |
curl (nativo) |
| Prueba de carga | 1..200 | ForEach-Object { curl.exe ... } |
for i in $(seq 1 200); do curl ... & done; wait |
| Variables | $env:VAR |
export VAR=... |
| Rutas | C:\Users\Admin\Documents\m3a3 |
~/Desktop/devs/aks-m3a3 |
- Consigna M3A3 — UAO Virtual
- Azure AKS Quickstart: https://learn.microsoft.com/en-us/azure/aks/learn/quick-kubernetes-deploy-cli
- AKS Store Demo: https://github.com/Azure-Samples/aks-store-demo
- PyTorch ResNet-18: https://pytorch.org/vision/stable/models/resnet.html
- Azure Container Insights: https://learn.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-overview
- Tutorial DL en Kubernetes (referencia original): https://opensource.com/article/20/9/deep-learning-model-kubernetes
- Instalar Azure CLI en macOS: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-macos
Documento generado el 10-Jun-2026 con la ejecución real de todos los comandos desde macOS Apple Silicon M1.