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 |
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
Plan d'implémentation :
stx deploy gcp(Cloud Run)Vue d'ensemble
gcp.py(~450 lignes)deploy_cmd.py,commands.py,coolify.py1. Nouveau fichier :
streamtex/cli/gcp.py1.1 Constants
1.2 Dataclasses
1.3 Classe
GCPClientToutes les opérations passent par
gcloudCLI (pas de SDK Python — cohérent avec l'usage dehcloudpour Hetzner) :1.4 Sérialisation dans DeployState
Étendre
DeployStatedanscoolify.pypour supporter les deux providers :Ajouter
providerdansAppEntry:Migration automatique :
from_dict()détecteversion: "2.0"→ migre vers 3.0, metprovider="hetzner"sur toutes les apps existantes.2. Commandes CLI
2.1
stx deploy gcp-setup— Configuration initialeCible : utilisateur partant de zéro OU utilisateur expérimenté
Flow détaillé (13 étapes) :
Gestion des utilisateurs expérimentés :
--project+--regionbypasse les prompts.stx-deploy.envcontient déjàGCP_PROJECT_ID: propose comme défautgcloud configa un projet actif : propose comme défaut2.2
stx deploy gcp— Déployer un projetFlow détaillé (14 étapes) :
2.3
stx deploy gcp-domain— Domaine customFlow :
2.4 Extensions des commandes existantes
stx deploy status gcpAjouter un scope
gcpdans la commande status existante :stx deploy update— Détection auto du providerstx deploy scale— Adapter pour Cloud Run3. 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 gcpsi l'utilisateur le souhaite :3.3 Secrets GitHub nécessaires
GCP_PROJECT_IDGCP_WIF_PROVIDERprojects/{num}/locations/global/workloadIdentityPools/{pool}/providers/{provider}gcloud iam workload-identity-pools providers describeGCP_SA_EMAILstreamtex-deploy@{project}.iam.gserviceaccount.comgcloud iam service-accounts listLa commande
stx deploy gcp-setuppeut guider la création de ces secrets en affichant les valeurs à copier.4. Tests
Fichier :
tests/test_cli_deploy_gcp.pyTous les tests mock
subprocess.run— aucun appel réel àgcloud.5. Modifications dans les fichiers existants
5.1
commands.py— Enregistrement5.2
coolify.py— DeployState v3gcp: GCPInfo | None = NonedansDeployStateprovider: str = ""dansAppEntryAppEntry:service_url,custom_domain,min_instances,max_instances,imagefrom_dict(): ajouterprovider="hetzner"aux apps existantesto_dict(): inclureinfrastructure.gcp5.3
deploy_cmd.py— Extensionsstatus_cmd: ajouter branche"gcp"dans le dispatcherupdate_cmd: détecterapp.provideret dispatcher vers GCP ou Coolifyscale_cmd:--min-instances/--max-instancespour GCP,--replicaspour Hetznerfrom .gcp import GCPClient, GCPError6. Parcours utilisateur
6.1 Utilisateur débutant (from zero)
6.2 Utilisateur expérimenté GCP
6.3 Multi-provider
7. Ordre d'implémentation
gcp.py: constants + dataclasses + GCPClient (sans deploy)coolify.py: DeployState v3 + migration + provider fielddeploy_cmd.py:stx deploy gcp-setupdeploy_cmd.py:stx deploy gcpdeploy_cmd.py: extensions status/update/scaledeploy_cmd.py:stx deploy gcp-domaintests/test_cli_deploy_gcp.pycommands.py: enregistrementTotal : ~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
Environment