🍕 De máquinas virtuales a serverless: Despliega tu primera aplicación en Google Cloud
Sala en vivo 0% completado
Práctica 0 · Configuración inicial (~15 min)

🍕 Preparando nuestra Cocina en Google Cloud

¡Bienvenido/a al taller! Durante las próximas 3 horas recorreremos la evolución de cómo llevar aplicaciones a producción: desde levantar una máquina virtual tradicional hasta usar contenedores, Kubernetes, plataformas serverless e Inteligencia Artificial. Todo desde el navegador, sin instalar nada en tu ordenador y sin necesitar tarjeta bancaria gracias a los créditos del taller.

Paso 0.1 — Reclamar tus 5$ de créditos gratuitos para el taller

  1. Accede a la URL de créditos promocionales: https://g.dev/credits/promo-c4c90cfc43cdbefa
  2. Inicia sesión con una cuenta personal de Gmail (@gmail.com).
    • ¿No tienes una cuenta personal de Gmail (solo tienes correo corporativo o universitario)? Créate una cuenta gratuita de Gmail ahora en un minuto para poder reclamar los créditos sin bloqueos de organización, o bien utiliza tu propia cuenta de facturación personal si ya dispones de una.
  3. Pulsa para reclamar los créditos y acepta las condiciones si se solicitan. Al terminar, Google habrá creado automáticamente en tu usuario una nueva cuenta de facturación con 5 créditos (5$) disponibles.
💡 Importante: Si tienes varias cuentas abiertas en el navegador (p. ej. la del trabajo y la personal), abre una ventana o perfil de Chrome donde la cuenta principal sea tu @gmail.com personal para evitar confusiones en el siguiente paso.

Paso 0.2 — Crear un Proyecto nuevo en la Consola de Google Cloud

En Google Cloud todos los recursos que creamos viven siempre dentro de un Proyecto. Vamos a crear uno limpio para este taller:

  1. Entra en https://console.cloud.google.com y comprueba en el avatar de arriba a la derecha que estás con la misma cuenta personal de Gmail con la que reclamaste los créditos. (Si es tu primera vez en la consola, acepta los Términos del Servicio).
  2. Arriba a la izquierda (justo a la derecha del logo Google Cloud), haz clic en el selector de proyectos (puede poner "Selecciona un proyecto" o "My First Project").
  3. En la esquina superior derecha de la ventana emergente que se abre, haz clic en "Proyecto nuevo" (New Project).
  4. Rellena el formulario:
    • Nombre del proyecto: Escribe por ejemplo taller-pizzeria-cloud.
    • Cuenta de facturación (Billing account): Si aparece este selector, elige la cuenta de créditos promocionales recién creada (suele llamarse Google Cloud Platform Trial Billing Account).
    • Ubicación: Déjalo en Sin organización.
  5. Haz clic en el botón azul "Crear" (Create), espera unos segundos a que se complete la notificación arriba a la derecha y pulsa "Seleccionar proyecto" (Select Project) para que tu nuevo proyecto aparezca activo arriba a la izquierda.

Paso 0.3 — Verificar que el proyecto está asociado a la cuenta de facturación de 5$

Antes de lanzar máquinas o contenedores, comprobemos que nuestro proyecto nuevo está conectado a la cuenta de facturación de los créditos gratuitos:

  1. Con tu proyecto taller-pizzeria-cloud seleccionado arriba a la izquierda, abre el menú principal (icono ☰ arriba a la izquierda) y haz clic en Facturación (Billing).
  2. Comprueba cuál de estas dos pantallas te aparece:

✅ Caso A: Ya aparece el panel de Resumen

Si ves directamente las gráficas de costes en 0,00 y en la columna derecha el bloque de Créditos promocionales (5$) activo, ¡tu proyecto ya está perfectamente vinculado!

🔗 Caso B: Dice "Este proyecto no tiene cuenta de facturación"

Haz clic en "Vincular una cuenta de facturación" (Link a billing account), selecciona en el desplegable la cuenta creada al canjear tus créditos (Google Cloud Platform Trial Billing Account) y pulsa "Establecer cuenta".

Paso 0.4 — Abrir Cloud Shell, confirmar facturación y encender los servicios

  1. En la barra superior derecha de la consola de Google Cloud, pulsa el icono de Activar Cloud Shell (>_). Se abrirá una terminal en la parte inferior; si aparece el botón azul Autorizar, púlsalo.
  2. Pulsa 📋 Copiar aquí abajo, pega el bloque en tu terminal de Cloud Shell y pulsa Enter:
Cloud Shell
export PROJECT_ID=$(gcloud config get-value project)
echo "🍕 Proyecto activo en Cloud Shell: $PROJECT_ID"

echo "💳 Comprobando que la cuenta de facturación de 5$ está vinculada (debe salir True):"
gcloud billing projects describe "$PROJECT_ID" --format="table(projectId, billingAccountName, billingEnabled)"

gcloud config set compute/region europe-west1
gcloud config set compute/zone europe-west1-b

gcloud services enable \
  compute.googleapis.com \
  container.googleapis.com \
  run.googleapis.com \
  cloudfunctions.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com

echo "✅ ¡Facturación verificada y servicios activados! Cocina lista."
👀 Verifica que en la salida aparece tu proyecto y BILLING_ENABLED: True. Si apareciera vacío o en False, revisa el desplegable de proyectos arriba a la izquierda o abre la ayuda de aquí abajo.
❓ ¿Cloud Shell no carga o no aparece tu proyecto seleccionado?
  • Si Cloud Shell se queda cargando: Asegúrate de no estar en ventana de Incógnito (o habilita cookies de terceros en cloud.google.com).
  • Si PROJECT_ID salió vacío: Selecciona tu proyecto en la parte superior de la consola o ejecuta gcloud projects list y luego gcloud config set project ID_DE_TU_PROYECTO y vuelve a lanzar el bloque anterior.
Práctica 1 · Infraestructura como Servicio (~20 min)

🖥️ Compute Engine: "Amasando y horneando en casa"

Empezamos por la opción más cercana al hierro: crear una Máquina Virtual Linux desde cero, entrar dentro por SSH para instalar un servidor web a mano y configurar el Firewall para permitir visitas.

Paso 1.1 — Crear nuestra máquina virtual pizzeria-vm

Cloud Shell
gcloud compute instances create pizzeria-vm \
  --zone=europe-west1-b \
  --machine-type=e2-micro \
  --image-family=debian-12 \
  --image-project=debian-cloud \
  --tags=http-server
👀 Abre el menú izquierdo de la consola web y entra en Compute Engine > Instancias de VM. Verás tu máquina encendida con su IP externa. Si intentas abrir esa IP ahora, verás que falla: ¡la máquina está vacía y con los puertos cerrados!

Paso 1.2 — Entrar por SSH e instalar el servidor web Nginx

Ejecuta este comando para conectarte a tu máquina, instalar Nginx y crear nuestra página web. (Si te pregunta Do you want to continue (Y/n)? escribe Y y pulsa Enter; si pide passphrase pulsa Enter dos veces dejándola vacía):

Cloud Shell
gcloud compute ssh pizzeria-vm --zone=europe-west1-b --command='
sudo apt-get update -y && sudo apt-get install -y nginx

sudo tee /var/www/html/index.html > /dev/null <<EOF
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Trattoria Cloud - Máquina Virtual</title>
  <style>
    body { font-family: sans-serif; background: #fff8f5; padding: 2rem; display: flex; justify-content: center; }
    .card { max-width: 650px; background: white; padding: 2rem; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); border-top: 8px solid #ea4335; }
    .meta { background: #f1f3f4; padding: 1rem; border-radius: 10px; font-family: monospace; margin: 1rem 0; }
  </style>
</head>
<body>
  <div class="card">
    <h1>🍕 Trattoria Cloud (Compute Engine)</h1>
    <p>¡Tu servidor web Nginx está funcionando dentro de tu Máquina Virtual!</p>
    <div class="meta">
      <strong>Máquina (Hostname):</strong> $(hostname)<br>
      <strong>Modo Pizza:</strong> Amasando y horneando en casa
    </div>
    <p>⚠️ <strong>El reto:</strong> Si esta máquina se cae a las 3:00 AM o entran 10.000 clientes a la vez, nos toca mantenerlo a nosotros.</p>
  </div>
</body>
</html>
EOF

sudo systemctl restart nginx
'

Paso 1.3 — Abrir la regla de Firewall (Puerto 80) y visitar la web

Cloud Shell
gcloud compute firewall-rules create permitir-http-pizzeria \
  --allow=tcp:80 \
  --target-tags=http-server \
  --description="Abre el puerto 80 para nuestra web en Compute Engine"

IP_VM=$(gcloud compute instances describe pizzeria-vm --zone=europe-west1-b --format='get(networkInterfaces[0].accessConfigs[0].natIP)')
echo "🎉 Abre este enlace en tu navegador -> http://$IP_VM"
❓ ¿Al abrir la IP no carga la página?
Asegúrate de usar http:// (sin la s del final), ya que en esta máquina virtual solo hemos abierto el puerto 80 sin configurar certificados SSL. Haz clic directamente en el enlace que imprimió el comando anterior en la terminal.

Paso 1.4 — Borrar la máquina virtual para cuidar nuestro crédito

💰 Las máquinas virtuales cobran por cada minuto que están encendidas, aunque no tengan visitas. Vamos a eliminarla antes de pasar al siguiente bloque:
Cloud Shell
gcloud compute instances delete pizzeria-vm --zone=europe-west1-b --quiet
Práctica 2 · Contenedores y Orquestación (~30 min)

🐳☸️ Contenedores y Kubernetes (GKE)

Vamos a empaquetar una aplicación web en un Contenedor Docker y desplegarla en un clúster real de Kubernetes (GKE) con 3 réplicas trabajando en paralelo, balanceo de carga y autocuración automática.

Paso 2.0 — 🔥 Crear el clúster de GKE (Lánzalo antes de la explicación teórica)

Aprovisionar un clúster de Kubernetes tarda unos 4–5 minutos. Ejecuta este comando ahora mismo y atiende a la explicación mientras Google Cloud prepara tus nodos:

Cloud Shell
gcloud container clusters create pizzeria-cluster \
  --zone=europe-west1-b \
  --num-nodes=2 \
  --machine-type=e2-small \
  --disk-type=pd-standard \
  --disk-size=20

Paso 2.1 — Crear nuestra aplicación y su Dockerfile

Cuando el comando anterior haya finalizado, crea la aplicación web. Esta app muestra el identificador y color del contenedor/Pod que responde cada petición:

Cloud Shell
mkdir -p ~/taller-cloud/lab2-gke && cd ~/taller-cloud/lab2-gke

cat << 'EOF' > requirements.txt
Flask==3.1.0
gunicorn==23.0.0
EOF

cat << 'EOF' > app.py
import os, socket, datetime
from flask import Flask, render_template_string

app = Flask(__name__)
CONTAINER_ID = socket.gethostname()
DEPLOY_MODE = os.environ.get("MODO_DESPLIEGUE", "Contenedor Docker")
PALETTE = ["#1a73e8", "#ea4335", "#34a853", "#9334e6", "#e8710a", "#00897b"]
POD_COLOR = PALETTE[sum(ord(c) for c in CONTAINER_ID) % len(PALETTE)]

HTML = """
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Trattoria Cloud</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f8f9fa; padding: 2rem; display: flex; justify-content: center; }
    .box { max-width: 680px; width: 100%; background: white; border-radius: 18px; padding: 2rem; box-shadow: 0 12px 30px rgba(0,0,0,0.08); border-top: 10px solid {{ color }}; }
    .badge { background: {{ color }}20; color: {{ color }}; padding: 0.4rem 0.9rem; border-radius: 999px; font-weight: 700; font-size: 0.85rem; }
    .pod { background: #202124; color: white; padding: 1.2rem; border-radius: 12px; margin: 1.5rem 0; display: flex; justify-content: space-between; align-items: center; }
    .pod-id { font-family: monospace; font-size: 1.3rem; color: #8ab4f8; font-weight: bold; }
    .btn { display: inline-block; background: {{ color }}; color: white; text-decoration: none; padding: 0.7rem 1.3rem; border-radius: 8px; font-weight: 600; }
  </style>
</head>
<body>
  <div class="box">
    <span class="badge">🚀 {{ mode }}</span>
    <h1>🍕 Trattoria Cloud</h1>
    <p>Especialidad: <strong>Pizza con Sobrasada y Queso de Mahón</strong></p>
    <div class="pod">
      <div>
        <div style="font-size:0.8rem; color:#9aa0a6;">ATENDIDO POR EL CONTENEDOR / POD:</div>
        <div style="font-size:0.85rem; color:#dadce0;">Recarga la página para ver qué réplica te atiende</div>
      </div>
      <div class="pod-id">{{ cid }}</div>
    </div>
    <p><small>Hora del servidor: {{ now }} UTC</small></p>
    <a class="btn" href="/">🔄 Pedir otra pizza (Recargar)</a>
  </div>
</body>
</html>
"""

@app.route("/")
def index():
    return render_template_string(HTML, cid=CONTAINER_ID, mode=DEPLOY_MODE, color=POD_COLOR, now=datetime.datetime.utcnow().strftime("%H:%M:%S"))

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
EOF

cat << 'EOF' > Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV PORT=8080
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]
EOF

echo "✅ Archivos listos"

Paso 2.2 — Construir la imagen y guardarla en Artifact Registry

Cloud Shell
export PROJECT_ID=$(gcloud config get-value project)

gcloud artifacts repositories create pizzeria-repo \
  --repository-format=docker \
  --location=europe-west1 \
  --description="Repositorio de contenedores del taller" || true

gcloud builds submit \
  --tag europe-west1-docker.pkg.dev/$PROJECT_ID/pizzeria-repo/pizzeria-web:v1 .

Paso 2.3 — Desplegar en Kubernetes, escalar a 3 réplicas y exponer al mundo

Cloud Shell
export PROJECT_ID=$(gcloud config get-value project)

kubectl get nodes

kubectl create deployment pizzeria-web \
  --image=europe-west1-docker.pkg.dev/$PROJECT_ID/pizzeria-repo/pizzeria-web:v1

kubectl set env deployment/pizzeria-web MODO_DESPLIEGUE="Google Kubernetes Engine (GKE)"
kubectl scale deployment pizzeria-web --replicas=3

kubectl expose deployment pizzeria-web \
  --type=LoadBalancer \
  --port=80 \
  --target-port=8080

Paso 2.4 — Ver la IP Pública y probar la Autocuración (Self-Healing)

Consulta tus 3 Pods y la dirección EXTERNAL-IP del balanceador:

Cloud Shell
kubectl get pods
kubectl get service pizzeria-web
❓ ¿En EXTERNAL-IP aparece <pending>?
Es totalmente normal: el Balanceador de Carga tarda unos 45 segundos en crearse. Espera medio minuto y vuelve a ejecutar kubectl get service pizzeria-web hasta que aparezca la IP numérica. Ábrela con http://TU_EXTERNAL_IP y pulsa varias veces "Recargar".

Ahora destruye uno de los Pods a propósito y comprueba cómo Kubernetes levanta uno nuevo al instante para mantener siempre 3 réplicas:

Cloud Shell
POD_A_BORRAR=$(kubectl get pods -o jsonpath='{.items[0].metadata.name}')
echo "💥 Eliminando el Pod: $POD_A_BORRAR"
kubectl delete pod $POD_A_BORRAR
kubectl get pods

Paso 2.5 — Eliminar el clúster GKE (¡Importante!)

💰 Paso obligatorio antes de continuar: Borramos el clúster y el balanceador de carga para que no sigan consumiendo tus 5$ de crédito:
Cloud Shell
gcloud container clusters delete pizzeria-cluster --zone=europe-west1-b --quiet
Práctica 3 · Serverless (~20 min)

⚡ Cloud Run y Cloud Run Functions: "Pidiendo a domicilio"

¿Y si pudieras desplegar exactamente ese mismo contenedor sin crear clústeres, con HTTPS incluido y pagando 0,00 $ cuando no hay visitas? Vamos a probar Cloud Run y Cloud Run Functions.

Paso 3.1 — Desplegar nuestro contenedor en Cloud Run con un solo comando

Cloud Shell
export PROJECT_ID=$(gcloud config get-value project)

gcloud run deploy pizzeria-serverless \
  --image=europe-west1-docker.pkg.dev/$PROJECT_ID/pizzeria-repo/pizzeria-web:v1 \
  --region=europe-west1 \
  --allow-unauthenticated \
  --port=8080 \
  --set-env-vars="MODO_DESPLIEGUE=Cloud Run (Serverless - Escalado a 0)"
🎉 En menos de 30 segundos aparecerá tu Service URL (https://pizzeria-serverless-...run.app). Haz clic en ella para abrir tu aplicación con HTTPS automático.

Paso 3.2 — Desplegar una Cloud Run Function (sin servidor ni Dockerfile)

Cuando solo necesitas ejecutar una pequeña función ante una petición o evento, ni siquiera hace falta un servidor completo:

Cloud Shell
mkdir -p ~/taller-cloud/lab3-functions && cd ~/taller-cloud/lab3-functions

cat << 'EOF' > requirements.txt
functions-framework==3.*
EOF

cat << 'EOF' > main.py
import math
import functions_framework

@functions_framework.http
def calcular_pizzas(request):
    amigos = int(request.args.get("amigos", 6))
    hambre = request.args.get("hambre", "normal").lower()
    porciones = {"poca": 2, "normal": 3, "mucha": 5}.get(hambre, 3)
    total_porciones = amigos * porciones
    pizzas = math.ceil(total_porciones / 8)

    return f"""
    <html><body style="font-family:sans-serif; padding:2rem; background:#f0fdf4;">
      <div style="max-width:520px; margin:auto; background:white; padding:2rem; border-radius:16px; border-top:8px solid #16a34a; box-shadow:0 8px 20px rgba(0,0,0,0.06);">
        <span style="background:#dcfce7; color:#15803d; padding:4px 10px; border-radius:99px; font-weight:bold; font-size:0.8rem;">⚡ CLOUD RUN FUNCTION</span>
        <h1>🍕 Calculadora Serverless de Pizzas</h1>
        <p>Para <b>{amigos} amigos</b> con hambre <b>{hambre}</b>:</p>
        <h2 style="color:#16a34a; font-size:2.2rem;">¡Pedid {pizzas} pizzas! ({total_porciones} porciones)</h2>
        <hr>
        <p>Prueba a cambiar los parámetros:</p>
        <a href="?amigos=4&hambre=poca">4 amigos (poca hambre)</a> ·
        <a href="?amigos=10&hambre=normal">10 amigos (hambre normal)</a> ·
        <a href="?amigos=18&hambre=mucha">18 amigos (mucha hambre)</a>
      </div>
    </body></html>
    """
EOF

gcloud functions deploy calculadora-pizzas \
  --gen2 \
  --runtime=python312 \
  --region=europe-west1 \
  --source=. \
  --entry-point=calcular_pizzas \
  --trigger-http \
  --allow-unauthenticated

Al terminar, abre la URL (uri) que aparece en el resultado y prueba los enlaces con distinto número de amigos.

Práctica 4 · Reto Final (~35 min)

🤖🚀 De unas líneas generadas con IA a una URL pública en Cloud Run

En este reto final crearás tu propia aplicación web apoyándote en Gemini, la probarás en el Editor Visual de Cloud Shell y la dejarás publicada en Internet con Cloud Run sin escribir ningún Dockerfile.

Paso 4.1 — Preparar la carpeta y abrir main.py en el Editor de Cloud Shell

Cloud Shell
mkdir -p ~/taller-cloud/lab4-mi-app-ia && cd ~/taller-cloud/lab4-mi-app-ia

cat << 'EOF' > requirements.txt
Flask==3.1.0
gunicorn==23.0.0
EOF

cat << 'EOF' > Procfile
web: gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
EOF

cat << 'EOF' > main.py
import os
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "<h1>¡Listo para pegar aquí el código generado con Gemini!</h1>"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
EOF

cloudshell edit main.py
✨ Se abrirá en la mitad superior el editor visual con el archivo main.py listo para editar.

Paso 4.2 — Pedir a Gemini que genere nuestra aplicación web

Abre gemini.google.com (o aistudio.google.com), copia este prompt, escribe tu propia idea entre los corchetes y envíalo:

Prompt para Gemini
Actúa como un desarrollador experto en Python y Google Cloud Run.
Quiero crear una aplicación web interactiva sobre: [ESCRIBE AQUÍ TU IDEA: por ejemplo, un votador en vivo de pizzas donde cualquiera pueda añadir su pizza favorita y votar las de los demás / un quiz interactivo con puntuación / una calculadora para dividir los gastos de un viaje entre amigos].

Por favor, genera el código cumpliendo ESTRICTAMENTE estas reglas técnicas para que funcione a la primera en Cloud Run:
1. Todo el código (backend en Flask + HTML, CSS moderno y JavaScript dentro de render_template_string) debe ir en UN ÚNICO archivo llamado `main.py`.
2. Usa únicamente la librería estándar de Python y `Flask` (guarda los datos en memoria en una lista o diccionario global de Python, sin bases de datos externas).
3. El diseño visual debe ser muy atractivo, moderno, con tarjetas, emojis y en español.
4. Al final del archivo, incluye obligatoriamente este bloque exacto para arrancar en el puerto que exige Cloud Run:
if __name__ == "__main__":
    import os
    port = int(os.environ.get("PORT", 8080))
    app.run(host="0.0.0.0", port=port)
5. Devuélveme únicamente el código completo de `main.py` listo para copiar y pegar.

Paso 4.3 — Probar tu aplicación antes de publicar

  1. Copia el código Python generado por Gemini, selecciona todo el contenido de main.py en el editor de Cloud Shell (Ctrl+A / Cmd+A), bórralo y pega tu nuevo código.
  2. En la terminal inferior, arráncalo en modo prueba:
Cloud Shell
cd ~/taller-cloud/lab4-mi-app-ia
python3 main.py

Haz clic en el icono de Vista previa web (Web Preview) arriba a la derecha de Cloud Shell y elige Vista previa en el puerto 8080. Cuando termines de verla, pulsa Ctrl+C en la terminal.

❓ ¿Te ha dado algún error de sintaxis el código de la IA o quieres probar una plantilla ya lista?
Asegúrate de no haber copiado las comillas triples ```python al principio o al final de main.py. Si quieres cargar directamente una app completa de Votación de Pizzas en Vivo para desplegarla ya, copia y ejecuta este bloque en la terminal:
Cargar app de ejemplo lista
cd ~/taller-cloud/lab4-mi-app-ia
cat << 'EOF' > main.py
import os
from flask import Flask, request, redirect, url_for, render_template_string

app = Flask(__name__)
PIZZAS = [
    {"id": 1, "nombre": "Mallorquina (Sobrasada y Queso de Mahón)", "autor": "Laura", "votos": 5},
    {"id": 2, "nombre": "Diavola al Horno de Leña", "autor": "Andreu", "votos": 4},
    {"id": 3, "nombre": "Cuatro Quesos Serverless", "autor": "Cloud Team", "votos": 3},
]
HTML = """<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><title>PizzaParty Live</title>
<style>body{font-family:sans-serif;background:#1e1b4b;padding:2rem;display:flex;justify-content:center;}
.card{max-width:640px;width:100%;background:white;padding:2rem;border-radius:18px;}
.item{display:flex;justify-content:space-between;align-items:center;background:#f8fafc;padding:1rem;border-radius:12px;margin:0.6rem 0;border:1px solid #e2e8f0;}
button{background:#4f46e5;color:white;border:none;padding:0.6rem 1rem;border-radius:8px;font-weight:bold;cursor:pointer;}
input{padding:0.6rem;border:1px solid #cbd5e1;border-radius:8px;margin-right:0.4rem;}</style></head>
<body><div class="card"><h1>🍕 Votación de Pizzas en Vivo</h1>
{% for p in pizzas %}<div class="item"><div><b>{{p.nombre}}</b><br><small>Por {{p.autor}}</small></div>
<form method="POST" action="/votar/{{p.id}}"><button>👍 {{p.votos}}</button></form></div>{% endfor %}
<hr><form method="POST" action="/proponer"><input name="nombre" placeholder="Tu pizza favorita" required>
<input name="autor" placeholder="Tu nombre" required><button type="submit">➕ Añadir</button></form></div></body></html>"""

@app.route("/")
def home():
    return render_template_string(HTML, pizzas=sorted(PIZZAS, key=lambda x: x["votos"], reverse=True))

@app.route("/votar/<int:pid>", methods=["POST"])
def votar(pid):
    for p in PIZZAS:
        if p["id"] == pid: p["votos"] += 1
    return redirect(url_for("home"))

@app.route("/proponer", methods=["POST"])
def proponer():
    PIZZAS.append({"id": len(PIZZAS)+1, "nombre": request.form.get("nombre","Pizza"), "autor": request.form.get("autor","Anónimo"), "votos": 1})
    return redirect(url_for("home"))

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
EOF

Paso 4.4 — ¡Publicar en Cloud Run con URL abierta para todos!

Ejecuta este comando para empaquetar tu código automáticamente con Buildpacks y desplegarlo en producción (si te pregunta si deseas crear un repositorio para código fuente, escribe Y y pulsa Enter):

Cloud Shell
cd ~/taller-cloud/lab4-mi-app-ia

gcloud run deploy mi-app-ia \
  --source . \
  --region=europe-west1 \
  --allow-unauthenticated
🎉 ¡Lo conseguiste! Abre tu nueva URL pública (https://mi-app-ia-...run.app) desde tu móvil o compártela con el resto del taller.
Cierre del Taller

🧹 Limpieza Final de Recursos (Opcional)

Los servicios desplegados en Cloud Run y Cloud Run Functions escalan automáticamente a 0 instancias cuando nadie los visita, por lo que puedes mantener tu URL activa para enseñársela a quien quieras casi sin coste.

Borrar todos los recursos restantes cuando ya no los necesites

Si quieres dejar tu proyecto de Google Cloud completamente limpio al acabar el día, ejecuta:

Cloud Shell
gcloud run services delete pizzeria-serverless --region=europe-west1 --quiet
gcloud run services delete mi-app-ia --region=europe-west1 --quiet
gcloud functions delete calculadora-pizzas --gen2 --region=europe-west1 --quiet
gcloud artifacts repositories delete pizzeria-repo --location=europe-west1 --quiet
gcloud compute firewall-rules delete permitir-http-pizzeria --quiet
🔒 Completa y marca todos los checks del paso anterior para desbloquear esta práctica.
🎤🍕

¡Miramos a la pantalla principal!

Estamos viendo la explicación teórica del siguiente ingrediente del taller.

✨ Tu progreso está guardado. En cuanto empiece el bloque práctico, esta pantalla se abrirá sola automáticamente.