Skip to content

Instantly share code, notes, and snippets.

@javier-henao
Last active June 11, 2026 11:10
Show Gist options
  • Select an option

  • Save javier-henao/a4fe8a3979f66b4d14e65ba9e72eee12 to your computer and use it in GitHub Desktop.

Select an option

Save javier-henao/a4fe8a3979f66b4d14e65ba9e72eee12 to your computer and use it in GitHub Desktop.
Guia para ejecutar actividad m3a3 en MacM1 con cuenta UAO

Guía Paso a Paso Ejecutada — M3A3: Kubernetes en Azure (AKS) desde Mac M1

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


Objetivo

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.


Stack utilizado

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

Arquitectura implementada

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

PARTE 0 — Preparación del entorno (macOS M1)

0.1 — Instalar/verificar Azure CLI y kubectl

brew update
brew install azure-cli
az version
# → "azure-cli": "2.87.0"

brew install kubectl
kubectl version --client
# → Client Version: v1.34.1

0.2 — Iniciar sesión en Azure

az 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: Enabled

PARTE 1 — Crear el Clúster AKS de 2 Nodos y Verificarlo

1.1 — Registrar proveedores de Azure (primera vez)

az 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
# → Registered

NOTA: Este paso solo es necesario la primera vez. Los proveedores quedan registrados permanentemente en la suscripción.

1.2 — Crear grupo de recursos

az group create --name rg-aks-m3a3 --location eastus2
# → "provisioningState": "Succeeded"

1.3 — Crear clúster AKS

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_B2s en eastus2. Se usó Standard_D2s_v3 (2 vCPU, 8 GB RAM), que es la opción más pequeña disponible.

1.4 — Verificar desde Azure CLI (Mac)

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://...

1.5 — Verificar desde Cloud Shell (Azure Portal)

  1. Ir a portal.azure.com
  2. Click en el ícono Cloud Shell (>_) en la barra superior
  3. Ejecutar:
az aks get-credentials --resource-group rg-aks-m3a3 --name aks-m3a3
kubectl get nodes
# → 2 nodos Ready

Evidencias: Capturas de terminal Mac y Cloud Shell mostrando 2 nodos Ready.


PARTE 2 — Clasificador de Imágenes (Deep Learning) en AKS

Problema y solución

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

2.1 — Crear la carpeta del proyecto

mkdir -p ~/Desktop/devs/aks-m3a3/clasificador
cd ~/Desktop/devs/aks-m3a3/clasificador

2.2 — Crear app.py (clasificador con PyTorch ResNet-18)

cat > 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)
PYEOF

2.3 — Crear requirements.txt y Dockerfile

cat > 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"]
EOF

2.4 — Crear deployment.yaml (ConfigMap + Deployment)

El 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-code

2.5 — Aplicar y verificar

kubectl 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)

2.6 — Exponer como LoadBalancer

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

2.7 — Probar el clasificador

# 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.223

Evidencias: Terminal con resultado de clasificación + imagen de prueba utilizada.


PARTE 3 — Aplicación de Interés (AKS Store Demo)

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.

3.1 — Desplegar

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)

3.2 — Obtener IP pública

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)

3.3 — Verificar en el navegador

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.


PARTE 4 — Supervisión y Monitoreo

4.1 — Métricas desde la terminal

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           3Mi

4.2 — Prueba de carga (200 peticiones concurrentes)

for 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.

4.3 — Vista "Insights" desde Azure Portal

  1. Portal de Azure → buscar clúster aks-m3a3
  2. Menú lateral → SuperviseInsights
  3. 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

4.4 — Métricas desde Azure Portal (Metrics)

  1. Menú del clúster → Metrics (Métricas)
  2. Agregar:
    • Metric: CPU Usage Percentage, Aggregation: Avg
    • Metric: Number of pods by phase, Aggregation: Avg

4.5 — Logs con Azure Log Analytics

  1. Menú del clúster → Records (Registros / Logs)
  2. Consultas KQL:

Inventario de pods por namespace:

KubePodInventory
| summarize Pods = dcount(Name) by Namespace

Resultado esperado: kube-system (~30), default (~9), tienda (~4)

Logs del clasificador (últimos 50):

ContainerLogV2
| where ContainerName has "kubermatic-dl"
| order by TimeGenerated desc
| take 50

Evidencias: Capturas de Insights (resumen clúster, CPU, memoria), Métricas (gráficas), Logs (KubePodInventory, ContainerLogV2), y terminal con kubectl top antes y después de la prueba de carga.


LIMPIEZA FINAL — Ahorro de créditos Azure

Opción A: Destruir todo

# 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-m3a3

Opción B: Solo detener el clúster (mantiene configuración)

az 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

Script de recreación rápida

#!/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

Archivos a conservar localmente

~/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

Errores encontrados y soluciones

# 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

Diferencias clave: Mac M1 vs Windows

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

Referencias


Documento generado el 10-Jun-2026 con la ejecución real de todos los comandos desde macOS Apple Silicon M1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment