Skip to content

feat: Support déploiement GCP Cloud Run (stx deploy gcp) #13

@nicolasguelfi

Description

@nicolasguelfi

Description

Ajouter le support du déploiement sur GCP Cloud Run comme alternative à Hetzner/Coolify, avec auto-scaling natif (0→N instances), scale-to-zero, et HTTPS automatique.

Motivation

  • Coût variable : Cloud Run facture à l'usage (0€ au repos) vs Hetzner à coût fixe — idéal pour les cours/formations avec pics ponctuels
  • Auto-scaling transparent : 10 à 1000 utilisateurs simultanés sans intervention manuelle
  • Pas de serveur à maintenir : pas de VM, pas de Coolify, pas de mises à jour sécurité
  • Complémentaire à Hetzner : la documentation permanente reste sur Hetzner (coût fixe imbattable), les projets à trafic variable vont sur GCP

Plan d'implémentation : stx deploy gcp (Cloud Run)

Vue d'ensemble

Élément Détail
Commandes 4 nouvelles sous-commandes
Client API 1 nouveau fichier gcp.py (~450 lignes)
Modifications deploy_cmd.py, commands.py, coolify.py
Tests 1 nouveau fichier (~250 lignes)
Templates Workflow GitHub Actions, guide utilisateur
Estimation totale ~1400 lignes

1. Nouveau fichier : streamtex/cli/gcp.py

1.1 Constants

DEFAULT_GCP_REGION = "europe-west1"           # Belgique
DEFAULT_GCP_REGISTRY = "docker.pkg.dev"
DEFAULT_MIN_INSTANCES = 0                     # Scale-to-zero
DEFAULT_MAX_INSTANCES = 10
DEFAULT_CPU = "1"
DEFAULT_MEMORY = "512Mi"
DEFAULT_TIMEOUT = 300                         # seconds
DEFAULT_CONCURRENCY = 80                      # max concurrent requests per container
CLOUD_RUN_PORT = 80                           # dual-mode Nginx

1.2 Dataclasses

@dataclass
class GCPInfo:
    """GCP project configuration stored in .stx-deploy.json"""
    project_id: str = ""
    region: str = DEFAULT_GCP_REGION
    registry: str = ""                        # {region}-docker.pkg.dev/{project}/{repo}
    auth_method: str = ""                     # "adc" | "service-account" | "workload-identity"

@dataclass
class GCPAppEntry:
    """A Cloud Run service in the state file"""
    name: str = ""
    service_url: str = ""                     # https://{name}-{hash}.run.app
    custom_domain: str = ""                   # https://docs.streamtex.org (optional)
    folder: str = ""
    image: str = ""                           # Full Artifact Registry image URI
    min_instances: int = DEFAULT_MIN_INSTANCES
    max_instances: int = DEFAULT_MAX_INSTANCES
    cpu: str = DEFAULT_CPU
    memory: str = DEFAULT_MEMORY
    serve_mode: str = "dual"
    deployed_at: str = ""
    concurrency: int = DEFAULT_CONCURRENCY

    @property
    def all_urls(self) -> list[str]:
        urls = []
        if self.service_url: urls.append(self.service_url)
        if self.custom_domain: urls.append(self.custom_domain)
        return urls

1.3 Classe GCPClient

Toutes les opérations passent par gcloud CLI (pas de SDK Python — cohérent avec l'usage de hcloud pour Hetzner) :

class GCPError(Exception):
    """GCP operation failed."""

class GCPClient:
    def __init__(self, project_id: str, region: str = DEFAULT_GCP_REGION):
        self.project_id = project_id
        self.region = region
        self._gcloud = shutil.which("gcloud")
        if not self._gcloud:
            raise GCPError("gcloud CLI not found. Install: https://cloud.google.com/sdk/install")

    @classmethod
    def from_env(cls, env_path=None) -> "GCPClient":
        """Create client from .stx-deploy.env or gcloud config.

        Search order:
        1. Explicit env_path
        2. .stx-deploy.env (GCP_PROJECT_ID, GCP_REGION)
        3. .stx-deploy.json infrastructure.gcp section
        4. `gcloud config get-value project` (active config)
        """

    # --- Artifact Registry ---
    def ensure_registry(self, repo_name="streamtex") -> str:
        """Create Artifact Registry repo if it doesn't exist.
        Returns: full registry path ({region}-docker.pkg.dev/{project}/{repo})
        """

    def build_and_push(self, path: str, image_tag: str, *, source_commit: str = "") -> str:
        """Build Docker image and push to Artifact Registry.
        Uses `gcloud builds submit` (Cloud Build) — no local Docker needed.
        Returns: full image URI
        """

    # --- Cloud Run ---
    def deploy(self, name: str, image: str, *,
               port: int = CLOUD_RUN_PORT,
               env_vars: dict[str, str] | None = None,
               min_instances: int = DEFAULT_MIN_INSTANCES,
               max_instances: int = DEFAULT_MAX_INSTANCES,
               cpu: str = DEFAULT_CPU,
               memory: str = DEFAULT_MEMORY,
               concurrency: int = DEFAULT_CONCURRENCY,
               allow_unauthenticated: bool = True) -> str:
        """Deploy or update a Cloud Run service.
        Returns: service URL
        """

    def update_env_vars(self, name: str, env_vars: dict[str, str]) -> None:
        """Update environment variables on a running service."""

    def get_service(self, name: str) -> dict:
        """Get Cloud Run service info (status, URL, instances, etc.)
        Uses: gcloud run services describe --format=json
        """

    def list_services(self, *, label_filter: str = "managed-by=streamtex") -> list[dict]:
        """List all StreamTeX-managed Cloud Run services.
        Uses label filter to exclude non-StreamTeX services.
        """

    def delete_service(self, name: str) -> None:
        """Delete a Cloud Run service."""

    def get_service_url(self, name: str) -> str:
        """Get the auto-generated URL for a service.
        Uses: gcloud run services describe --format='value(status.url)'
        """

    def wait_healthy(self, name: str, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = 10) -> bool:
        """Poll service until it responds to health checks.
        HTTP GET on service URL + /_stcore/health
        """

    # --- Scaling ---
    def scale(self, name: str, *, min_instances: int, max_instances: int) -> None:
        """Update auto-scaling bounds for a service.
        Uses: gcloud run services update --min-instances --max-instances
        """

    # --- Domain mapping ---
    def map_domain(self, name: str, domain: str) -> None:
        """Map a custom domain to a Cloud Run service.
        Uses: gcloud run domain-mappings create
        Requires: domain verification in Google Search Console
        """

    def list_domain_mappings(self) -> list[dict]:
        """List existing domain mappings."""

    # --- Verification ---
    def verify_auth(self) -> bool:
        """Check gcloud is authenticated and project is set.
        Uses: gcloud auth print-access-token (returns True if no error)
        """

    def check_apis(self) -> dict[str, bool]:
        """Check which required APIs are enabled.
        Returns: {"run.googleapis.com": True, "artifactregistry.googleapis.com": False, ...}
        """

    def enable_apis(self, apis: list[str]) -> None:
        """Enable required GCP APIs.
        Uses: gcloud services enable {api}
        """

    # --- Internal ---
    def _run(self, args: list[str], *, timeout: int = 120, check: bool = True) -> subprocess.CompletedProcess:
        """Run gcloud command with project and region flags."""
        cmd = [self._gcloud, *args, "--project", self.project_id, "--format", "json"]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        if check and result.returncode != 0:
            raise GCPError(f"gcloud {' '.join(args)} failed:\n{result.stderr.strip()}")
        return result

1.4 Sérialisation dans DeployState

Étendre DeployState dans coolify.py pour supporter les deux providers :

@dataclass
class DeployState:
    version: str = "3.0"              # Bump de 2.0 à 3.0
    provider: str = ""                # "hetzner" | "gcp" | "" (multi-provider)
    server: ServerInfo | None = None  # Hetzner
    domain: DomainInfo | None = None
    coolify: CoolifyInfo | None = None
    gcp: GCPInfo | None = None        # Nouveau
    applications: list[AppEntry] | None = None
    phases_completed: dict[str, str] | None = None
    cdn: dict | None = None
    security: dict | None = None

Ajouter provider dans AppEntry :

@dataclass
class AppEntry:
    name: str = ""
    uuid: str = ""                    # Coolify UUID (vide pour GCP)
    provider: str = ""                # "hetzner" | "gcp"
    # ... champs existants ...
    # Champs GCP (ignorés pour Hetzner)
    service_url: str = ""
    custom_domain: str = ""
    min_instances: int = 0
    max_instances: int = 10
    image: str = ""

Migration automatique : from_dict() détecte version: "2.0" → migre vers 3.0, met provider="hetzner" sur toutes les apps existantes.


2. Commandes CLI

2.1 stx deploy gcp-setup — Configuration initiale

Cible : utilisateur partant de zéro OU utilisateur expérimenté

@click.command("gcp-setup")
@click.option("--project", default=None, help="GCP project ID (skip prompt).")
@click.option("--region", default=DEFAULT_GCP_REGION, help="GCP region.")
def gcp_setup_cmd(project: str | None, region: str) -> None:
    """Interactive setup for GCP Cloud Run deployment."""

Flow détaillé (13 étapes) :

Phase A — Prérequis CLI

1. Vérifier gcloud CLI installé
   → Si absent : afficher lien https://cloud.google.com/sdk/install
   → Si macOS : proposer `brew install google-cloud-sdk`

2. Vérifier authentification gcloud
   → `gcloud auth print-access-token` (silent check)
   → Si pas authentifié : dire à l'utilisateur de lancer `! gcloud auth login`
     (doit être interactif — navigateur)

3. Vérifier Application Default Credentials (ADC)
   → `gcloud auth application-default print-access-token`
   → Si absent : dire de lancer `! gcloud auth application-default login`

Phase B — Projet GCP

4. Résoudre le projet GCP :
   → Si --project fourni : utiliser directement
   → Si GCP_PROJECT_ID dans .stx-deploy.env : proposer comme défaut
   → Sinon : lire `gcloud config get-value project`
   → Si toujours vide : lister les projets (`gcloud projects list`) et proposer
   → Permettre de créer un nouveau projet : `gcloud projects create {id}`

5. Vérifier la facturation
   → `gcloud billing projects describe {project}`
   → Si pas de compte de facturation : guider vers console.cloud.google.com/billing
   → Afficher un WARNING : "Cloud Run est facturé à l'usage. Voir les tarifs..."

Phase C — APIs

6. Vérifier les APIs requises :
   - run.googleapis.com (Cloud Run)
   - artifactregistry.googleapis.com (Container Registry)
   - cloudbuild.googleapis.com (Cloud Build)

7. Activer les APIs manquantes :
   → `gcloud services enable {api}` pour chaque API manquante
   → Confirmation avant activation

Phase D — Artifact Registry

8. Créer le dépôt Artifact Registry :
   → `gcloud artifacts repositories create streamtex
       --repository-format=docker --location={region}`
   → Si existe déjà : OK (idempotent)

9. Configurer l'auth Docker pour Artifact Registry :
   → `gcloud auth configure-docker {region}-docker.pkg.dev`

Phase E — Domaine (optionnel)

10. Proposer la configuration de domaine custom :
    → "Souhaitez-vous configurer un domaine custom ? (optionnel)"
    → Si oui : prompt pour le domaine
    → Vérifier dans Google Search Console
    → Ou proposer d'utiliser le domaine .run.app auto-généré

Phase F — Sauvegarde

11. Écrire .stx-deploy.env :
    GCP_PROJECT_ID={project}
    GCP_REGION={region}
    GCP_REGISTRY={region}-docker.pkg.dev/{project}/streamtex

12. Mettre à jour .stx-deploy.json :
    infrastructure.gcp = {project_id, region, registry, auth_method: "adc"}
    phases_completed.gcp_setup = timestamp

13. Afficher "Next steps" :
    → stx deploy gcp ./ (déployer un projet)

Gestion des utilisateurs expérimentés :

  • --project + --region bypasse les prompts
  • Si .stx-deploy.env contient déjà GCP_PROJECT_ID : propose comme défaut
  • Si gcloud config a un projet actif : propose comme défaut
  • Chaque étape vérifie avant d'agir (idempotent)

2.2 stx deploy gcp — Déployer un projet

@click.command("gcp")
@click.argument("path", default=".")
@click.option("--name", default=None, help="Cloud Run service name (default: directory name).")
@click.option("--subdomain", default=None, help="Custom domain subdomain (optional).")
@click.option("--serve-mode", type=click.Choice(SERVE_MODES), default="dual")
@click.option("--min-instances", type=int, default=0, help="Minimum containers (0=scale-to-zero).")
@click.option("--max-instances", type=int, default=10, help="Maximum containers.")
@click.option("--cpu", default="1", help="vCPUs per container.")
@click.option("--memory", default="512Mi", help="Memory per container.")
@click.option("--yes", is_flag=True)
def gcp_cmd(path, name, subdomain, serve_mode, min_instances, max_instances, cpu, memory, yes):
    """Deploy a StreamTeX project to GCP Cloud Run."""

Flow détaillé (14 étapes) :

1. Lire .stx-deploy.env → GCP_PROJECT_ID, GCP_REGION, GCP_REGISTRY
   → Si absent : "Run stx deploy gcp-setup first"

2. Preflight checks
   → Même _assert_preflight() que Hetzner

3. Générer fichiers de déploiement si absents
   → Dockerfile, nginx.conf, entrypoint.sh (même _ensure_deploy_files())

4. Résoudre le nom du service
   → --name ou slugify(basename(path))
   → Cloud Run naming rules : lowercase, hyphens, max 63 chars
   → Validation : re.match(r'^[a-z]([a-z0-9-]*[a-z0-9])?$', name)

5. Afficher la configuration
   → Table Rich : name, region, serve-mode, min/max instances, CPU, memory
   → Coût estimé : "Scale-to-zero: 0€ au repos, ~0.09€/heure sous charge"

6. Confirmation (sauf --yes)

7. Build & push l'image Docker
   → Méthode A (Cloud Build — recommandée) :
     `gcloud builds submit --tag {registry}/{name}:{commit_sha} {path}`
     (build distant, pas besoin de Docker local)
   → Méthode B (Docker local — fallback) :
     `docker build -t {registry}/{name}:{sha} {path}`
     `docker push {registry}/{name}:{sha}`
   → Ajout du label `managed-by=streamtex` sur l'image

8. Déployer sur Cloud Run
   → `gcloud run deploy {name}
       --image {image}
       --region {region}
       --platform managed
       --allow-unauthenticated
       --port {80 si dual, 8501 si streamlit-only}
       --cpu {cpu} --memory {memory}
       --min-instances {min} --max-instances {max}
       --concurrency {80}
       --set-env-vars "STX_SERVE_MODE={mode}"
       --labels "managed-by=streamtex"
       --timeout 300`

   → Si FOLDER nécessaire (sous-répertoire de modules/) :
     ajouter --set-env-vars "FOLDER={folder}"

9. Récupérer l'URL du service
   → `gcloud run services describe {name} --format='value(status.url)'`
   → URL auto : https://{name}-{hash}-{region}.a.run.app

10. Attendre que le service soit healthy
    → HTTP GET sur {url}/_stcore/health (ou /html/ pour dual)
    → Timeout : 300s, poll : 10s

11. Smoke tests post-deploy
    → Même pattern que _smoke_test_deploy() de Hetzner
    → Root URL → 200
    → /html/ → Nginx (si dual)

12. Mapper un domaine custom (si --subdomain)
    → `gcloud run domain-mappings create --service {name} --domain {sub}.{domain}`
    → Afficher les records DNS à créer (CNAME ghs.googlehosted.com)
    → Si Cloudflare token disponible : créer automatiquement

13. Sauvegarder l'état
    → Ajouter dans applications[] :
      {name, provider: "gcp", service_url, custom_domain, folder,
       image, min_instances, max_instances, serve_mode, deployed_at}
    → phases_completed.gcp_deploy = timestamp

14. Afficher le résultat
    → URL du service (run.app)
    → URL custom (si configurée)
    → Commande pour voir les logs : gcloud run logs read {name}

2.3 stx deploy gcp-domain — Domaine custom

@click.command("gcp-domain")
@click.argument("service_name")
@click.option("--domain", required=True, help="Custom domain (e.g. docs.streamtex.org).")
@click.option("--yes", is_flag=True)
def gcp_domain_cmd(service_name, domain, yes):
    """Map a custom domain to a GCP Cloud Run service."""

Flow :

1. Vérifier que le service existe
2. Créer le domain mapping :
   `gcloud run domain-mappings create --service {name} --domain {domain}`
3. Afficher les records DNS nécessaires :
   CNAME {domain} → ghs.googlehosted.com
4. Si Cloudflare token : créer le record automatiquement
5. Attendre la propagation DNS
6. Vérifier le SSL (Let's Encrypt via Google)
7. Mettre à jour .stx-deploy.json (custom_domain)

2.4 Extensions des commandes existantes

stx deploy status gcp

Ajouter un scope gcp dans la commande status existante :

# Dans status_cmd(), ajouter le cas "gcp" :
elif target == "gcp":
    client = GCPClient.from_env()
    services = client.list_services(label_filter="managed-by=streamtex")

    table = Table(title="GCP Cloud Run Services")
    table.add_column("Name", style="cyan")
    table.add_column("Status")
    table.add_column("Instances", style="dim")
    table.add_column("URL")
    table.add_column("Custom Domain")
    table.add_column("Deployed", style="dim")

    for svc in services:
        status_icon = "✓" if svc["ready"] else "⚠"
        instances = f"{svc['active']}/{svc['min']}-{svc['max']}"
        table.add_row(svc["name"], status_icon, instances, svc["url"], svc.get("domain", ""), ...)

    console.print(table)

stx deploy update — Détection auto du provider

# Dans update_cmd(), après résolution du target :
app = _find_app_in_state(target)
if app.provider == "gcp":
    client = GCPClient.from_env()
    # Re-build & push image, then deploy
    client.build_and_push(path, image_tag)
    client.deploy(app.name, image_tag, ...)
elif app.provider == "hetzner":
    # Existing Coolify logic
    ...

stx deploy scale — Adapter pour Cloud Run

# Dans scale_cmd(), détection du provider :
app = _find_app_in_state(target)
if app.provider == "gcp":
    client = GCPClient.from_env()
    client.scale(app.name, min_instances=min_inst, max_instances=max_inst)
    console.print(f"[green]✓ Scaling set: {min_inst}-{max_inst} instances[/green]")
elif app.provider == "hetzner":
    # Existing Coolify replica logic
    ...

3. Fichiers de déploiement

3.1 Dockerfile — Aucune modification

Le Dockerfile dual-mode existant fonctionne tel quel sur Cloud Run. Le port 80 (Nginx) est exposé, Cloud Run le détecte.

3.2 Template workflow GitHub Actions

Fichier template à générer par stx deploy gcp si l'utilisateur le souhaite :

# .github/workflows/gcp-deploy.yml
name: Deploy to GCP Cloud Run

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  REGION: europe-west1
  SERVICE: <service-name>
  REGISTRY: europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/streamtex

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write    # Workload Identity Federation

    steps:
      - uses: actions/checkout@v4

      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
          service_account: ${{ secrets.GCP_SA_EMAIL }}

      - uses: google-github-actions/setup-gcloud@v2

      - name: Build and push to Artifact Registry
        run: |
          gcloud builds submit \
            --tag "$REGISTRY/$SERVICE:${{ github.sha }}" \
            --project "$PROJECT_ID"

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy "$SERVICE" \
            --image "$REGISTRY/$SERVICE:${{ github.sha }}" \
            --region "$REGION" \
            --platform managed \
            --allow-unauthenticated \
            --port 80 \
            --set-env-vars "STX_SERVE_MODE=dual,SOURCE_COMMIT=${{ github.sha }}"

3.3 Secrets GitHub nécessaires

Secret Valeur Source
GCP_PROJECT_ID ID du projet GCP Console GCP
GCP_WIF_PROVIDER projects/{num}/locations/global/workloadIdentityPools/{pool}/providers/{provider} gcloud iam workload-identity-pools providers describe
GCP_SA_EMAIL streamtex-deploy@{project}.iam.gserviceaccount.com gcloud iam service-accounts list

La commande stx deploy gcp-setup peut guider la création de ces secrets en affichant les valeurs à copier.


4. Tests

Fichier : tests/test_cli_deploy_gcp.py

"""Tests for GCP Cloud Run deployment — streamtex.cli.gcp"""

class TestGCPConstants:
    def test_default_region(self): ...
    def test_serve_modes(self): ...

class TestGCPInfo:
    def test_defaults(self): ...
    def test_serialization(self): ...

class TestGCPAppEntry:
    def test_defaults(self): ...
    def test_all_urls(self): ...
    def test_serialization_roundtrip(self): ...

class TestGCPClient:
    def test_from_env_with_deploy_env(self): ...
    def test_from_env_with_gcloud_config(self): ...
    def test_from_env_missing_raises(self): ...
    def test_verify_auth(self): ...
    def test_check_apis(self): ...
    def test_deploy_builds_correct_command(self): ...
    def test_deploy_with_env_vars(self): ...
    def test_deploy_with_folder(self): ...
    def test_scale_command(self): ...
    def test_get_service_url(self): ...
    def test_list_services_with_label(self): ...
    def test_build_and_push_command(self): ...
    def test_map_domain_command(self): ...
    def test_service_name_validation(self): ...

class TestDeployStateV3Migration:
    def test_v2_migrated_to_v3(self): ...
    def test_v2_apps_get_hetzner_provider(self): ...
    def test_v3_with_gcp_info(self): ...
    def test_mixed_providers(self): ...

Tous les tests mock subprocess.run — aucun appel réel à gcloud.


5. Modifications dans les fichiers existants

5.1 commands.py — Enregistrement

from .deploy_cmd import gcp_cmd as deploy_gcp
from .deploy_cmd import gcp_setup_cmd as deploy_gcp_setup
from .deploy_cmd import gcp_domain_cmd as deploy_gcp_domain

deploy.add_command(deploy_gcp)
deploy.add_command(deploy_gcp_setup)
deploy.add_command(deploy_gcp_domain)

5.2 coolify.py — DeployState v3

  • Ajouter champ gcp: GCPInfo | None = None dans DeployState
  • Ajouter champ provider: str = "" dans AppEntry
  • Ajouter champs GCP dans AppEntry : service_url, custom_domain, min_instances, max_instances, image
  • Migration v2 → v3 dans from_dict() : ajouter provider="hetzner" aux apps existantes
  • Sérialisation v3 dans to_dict() : inclure infrastructure.gcp

5.3 deploy_cmd.py — Extensions

  • status_cmd : ajouter branche "gcp" dans le dispatcher
  • update_cmd : détecter app.provider et dispatcher vers GCP ou Coolify
  • scale_cmd : --min-instances / --max-instances pour GCP, --replicas pour Hetzner
  • Imports : ajouter from .gcp import GCPClient, GCPError

6. Parcours utilisateur

6.1 Utilisateur débutant (from zero)

# 1. Installer gcloud CLI
brew install google-cloud-sdk       # macOS
# ou suivre https://cloud.google.com/sdk/install

# 2. Se connecter
gcloud auth login                    # navigateur s'ouvre
gcloud auth application-default login

# 3. Setup StreamTeX pour GCP
stx deploy gcp-setup
# → Guided wizard :
#   ✓ gcloud CLI found
#   ✓ Authenticated as nicolas@...
#   ? GCP Project ID: [streamtex-prod] ▸ Enter
#   ✓ Billing account linked
#   ✓ APIs enabled: Cloud Run, Artifact Registry, Cloud Build
#   ✓ Registry created: europe-west1-docker.pkg.dev/streamtex-prod/streamtex
#   ✓ Configuration saved to .stx-deploy.env

# 4. Déployer
stx deploy gcp ./
# → Build + push + deploy + smoke test
# → URL: https://my-project-abc123-ew.a.run.app

# 5. (Optionnel) Domaine custom
stx deploy gcp-domain my-project --domain docs.streamtex.org

6.2 Utilisateur expérimenté GCP

# Skip le wizard — tout en flags
stx deploy gcp-setup --project my-existing-project --region us-central1

# Déployer avec config custom
stx deploy gcp ./ \
  --name my-course \
  --min-instances 1 \
  --max-instances 50 \
  --cpu 2 \
  --memory 1Gi \
  --yes

# Scaling pour un événement
stx deploy scale my-course --min 5 --max 100

# Après l'événement
stx deploy scale my-course --min 0 --max 10

6.3 Multi-provider

# Voir tout
stx deploy status              # Affiche Hetzner ET GCP

# Déployer la même app sur les deux
stx deploy hetzner ./           # → docs.streamtex.org (Hetzner)
stx deploy gcp ./               # → docs-abc123.run.app (GCP)

7. Ordre d'implémentation

Phase Tâche Dépendances Lignes
A gcp.py : constants + dataclasses + GCPClient (sans deploy) Aucune ~250
B coolify.py : DeployState v3 + migration + provider field Phase A ~80
C deploy_cmd.py : stx deploy gcp-setup Phase A ~200
D deploy_cmd.py : stx deploy gcp Phase A, B, C ~250
E deploy_cmd.py : extensions status/update/scale Phase B, D ~100
F deploy_cmd.py : stx deploy gcp-domain Phase D ~80
G tests/test_cli_deploy_gcp.py Phase A-F ~250
H commands.py : enregistrement Phase C, D, F ~10
I Template workflow GitHub Actions Phase D ~60
J Documentation skills Claude Phase A-I ~100

Total : ~1380 lignes

La Phase A-C est le socle (testable en isolation). La Phase D est le cœur. Les Phases E-J sont des extensions.


8. Estimation des coûts Cloud Run

Scénario Containers Coût
TP de 2h (1000 étudiants) ~13 ~2.50 €
Journée formation (8h, 1000) ~13 ~10 €
Mois entier (20j × 4h, 1000) ~13 ~95 €
Hors pic (0 users) 0 0 €

Environment

Key Value
StreamTeX 0.6.2
Python 3.10.8
OS Darwin 25.3.0 arm64
UV 0.10.2
Branch main
Commit c24a58b

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions