Vai al contenuto
Liguori
§ 04 | JOURNAL
Costruisco automazioni AI con Claude per PMI e freelancer italiani. 35+ automazioni in produzione, circa 70 job schedulati, zero dipendenti. Stack: Python, GCP.
ACCEPTING NEW CLIENTS
Cover Editorial Paper — Come Deployare Automazioni Claude su Google Cloud Run: Guida Pratica

Deploy di automazioni Claude su Cloud Run: il free tier regge 88 job, lo scheduler te ne regala tre

27 luglio 2026|27 min di lettura|Giovanni Liguori

TL;DR

Come deployare automazioni Claude su Google Cloud Run: containerizzazione, CI/CD, cron job e monitoring. Guida pratica con codice.

2.700 vCPU-secondi al mese. Questo consuma un'automazione Claude che parte una volta al giorno, gira novanta secondi e si spegne, su un Cloud Run Job da 1 vCPU. Il free tier mensile dei Job ne copre 240.000.

Fatta la divisione: quell'automazione occupa l'1,125% della quota gratuita di calcolo. Ne puoi mettere in piedi ottantotto dello stesso profilo prima di vedere un centesimo di CPU in fattura.

Poi apri Cloud Scheduler e scopri che i job schedulati gratuiti sono tre al mese, contati per account di fatturazione e non per progetto. Il quarto orario che imposti costa il doppio di tutto il calcolo che hai appena fatto girare.

Questa guida porta uno script Python dal Mac a un job schedulato su Google Cloud Run: Dockerfile, Artifact Registry, Secret Manager, Cloud Scheduler. I comandi sono verificati su gcloud 566.0.0, e i conti sono rifatti sulla tabella dei Jobs, che è una tabella diversa da quella dei Services e con numeri diversi.

Perché Cloud Run per le automazioni AI

Un'automazione basata su Claude ha un profilo di carico particolare: sta ferma per 23 ore e 58 minuti, lavora per 90 secondi, torna ferma.

Questo profilo rompe il modello di costo di tutto ciò che è sempre acceso. Un VPS da 5 euro al mese costa 5 euro anche nelle 23 ore e 58 minuti in cui non fa niente. Stai pagando il 99,9% del tempo per il privilegio di essere pronto durante lo 0,1%.

Cloud Run fattura la vita dell'istanza, e se l'istanza non parte non c'è niente da fatturare.

Qui va messa subito una precisazione, perché è il punto dove il modello mentale si rompe. I Job hanno un minimo di fatturazione di un minuto per esecuzione. La documentazione è letterale: "Cloud Run jobs are billed at the Instance-based billing rate, for the entire lifetime of any instance started, with a minimum of 1 minute". L'arrotondamento ai 100 millisecondi che trovi citato quasi ovunque riguarda i Services, non i Job. Per l'esempio di questa guida non cambia niente, perché 90 secondi stanno sopra il minuto. Per un job da 8 secondi cambia parecchio: paghi 60 secondi, sette volte e mezza quello che consumi.

I motivi concreti per cui resta la scelta giusta in questo scenario:

  • Scala a zero davvero. Nessuna istanza minima tenuta calda, nessun piano base sotto. Zero esecuzioni, zero costo di calcolo.
  • Il free tier è tarato molto sopra questo carico. Per i Job, Google copre ogni mese i primi 240.000 vCPU-secondi e i primi 450.000 GiB-secondi.
  • Container standard, nessun lock-in di formato. Quello che deployi è un'immagine Docker normale. Se domani la sposti su un altro runtime, sposti la stessa immagine.
  • Niente sistema operativo da mantenere. Nessuna patch di sicurezza da applicare, nessun kernel da aggiornare. E nessun disco che si riempie di log mentre sei in ferie ad agosto.

Il confronto con le alternative, senza tifoserie:

  • AWS Lambda ha lo stesso modello scale-to-zero ed è ottimo, ma il limite di durata dell'esecuzione è più stretto e il packaging è meno banale se le tue dipendenze Python sono pesanti. Con l'SDK Anthropic più qualche libreria di parsing ci si arriva prima di quanto pensi.
  • Azure Functions ha senso se il resto della tua vita è già su Azure. Se non lo è, stai importando un ecosistema intero per uno script.
  • Railway e simili sono più semplici da usare, e per il primo progetto vanno benissimo. Il collo di bottiglia arriva dopo, sulla gestione dei secret e sul controllo fine di memoria e timeout.
  • Cron sul tuo Mac è gratis e istantaneo, e per certi task resta la risposta giusta. Ho scritto un pezzo intero su quando il cron locale batte il cloud. Il limite è brutale e noto: se la macchina è spenta, il task non è in ritardo, semplicemente non è mai esistito.

Jobs o Services: la distinzione che decide tutto il resto

Sbagliare questa scelta significa pagare 24 ore al giorno per un lavoro che dura un minuto e mezzo.

Cloud Run espone due primitive diverse:

  • Cloud Run Jobs. Il container parte, fa il lavoro, termina. Non ascolta su una porta, non ha un URL pubblico, non aspetta richieste. È la forma giusta per batch, scraper, scanner di feed, generazione di report, sincronizzazioni notturne.
  • Cloud Run Services. Il container resta in ascolto su una porta HTTP e risponde a richieste. È la forma giusta per API, webhook, endpoint che qualcuno chiama dall'esterno.

La regola pratica: se stai schedulando qualcosa, vuoi un Job. Se qualcun altro deve chiamarti, vuoi un Service.

Vale la pena dirlo perché la scorciatoia è tentante: si può deployare un Service e tenerlo vivo con un ping ogni cinque minuti per evitare il cold start. Funziona, e costa come un server acceso. Se il tuo script parte alle 7:00 e finisce alle 7:01, due secondi di cold start non li nota nessuno.

Il resto di questa guida costruisce un Job.

Prerequisiti: cosa ti serve davvero

La lista è corta e si mette in piedi in un pomeriggio, la prima volta. Dalla seconda in poi sono minuti.

  1. Un account Google Cloud con fatturazione attiva. La fatturazione va abilitata anche per restare dentro il free tier: senza carta collegata, i servizi non partono. Il free tier si rinnova ogni mese e non scade come un trial.
  2. Un progetto GCP dedicato. Non buttare le automazioni dentro un progetto esistente pieno di altra roba. Un progetto separato ti dà un conto separato, quote separate e la possibilità di cancellare tutto con un comando quando l'esperimento non funziona.
  3. `gcloud` CLI installata e autenticata. Su macOS si installa con l'installer ufficiale o via Homebrew. Dopo l'installazione servono gcloud auth login per l'identità e gcloud config set project IL_TUO_PROJECT_ID per non doverlo ripetere ogni volta.
  4. Docker in locale. Serve per provare l'immagine prima di spedirla. Tecnicamente puoi saltarlo, perché il build può girare interamente sui server Google, ma testare in locale un'immagine rotta costa secondi invece che minuti.
  5. Una API key Anthropic. La prendi dalla console Anthropic. Non metterla in un file che finisce in git e non incollarla in una variabile d'ambiente in chiaro sul job: più avanti c'è la sezione su Secret Manager, che è il modo giusto.
  6. Python 3.11 o superiore. Non è un vincolo di Cloud Run, che esegue qualsiasi container. È solo la versione su cui è tarato l'esempio.

Le API da abilitare sul progetto sono cinque, e si attivano in un colpo solo:

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
  cloudscheduler.googleapis.com \
  secretmanager.googleapis.com

Se salti questo passaggio, il primo comando di deploy fallisce con un errore che parla di API non abilitata e ti propone di abilitarla al volo. Funziona anche così, ma rispondere a un prompt interattivo per ogni API rende il processo non ripetibile dentro uno script.

Step 1: l'automazione in locale, prima di pensare al cloud

Regola che vale più di tutte le altre messe insieme: non si deploya uno script che non ha mai girato bene in locale. Il cloud non aggiusta la logica, aggiunge solo un layer in cui il debug costa di più.

La struttura minima di progetto è questa:

news-scout/
├── Dockerfile
├── requirements.txt
├── script.py
└── .dockerignore

Quattro file. Niente cartelle src/, niente package, niente setup.py. Per un'automazione da un file la cerimonia è costo puro.

Il requirements.txt per un news scanner che usa Claude:

anthropic
feedparser
requests
python-dotenv

Lo script vero e proprio. L'esempio legge dei feed RSS, chiede a Claude di dare un punteggio di rilevanza ai titoli, e tiene solo quello che supera la soglia:

import os
import sys
import json
import re
import feedparser
from anthropic import Anthropic

client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5")
SOGLIA = int(os.environ.get("SOGLIA_RILEVANZA", "7"))
BATCH = 40

FEEDS = [
    "https://example.com/feed.xml",
    "https://example.org/rss",
]

def raccogli_titoli():
    voci = []
    for url in FEEDS:
        try:
            parsed = feedparser.parse(url)
            for entry in parsed.entries[:20]:
                voci.append({"titolo": entry.title, "link": entry.link})
        except Exception as e:
            print(f"feed non raggiungibile {url}: {e}", file=sys.stderr)
    return voci

def estrai_json(testo):
    """Il modello a volte incornicia il JSON in prosa o dentro un fence."""
    testo = re.sub(r"^```(?:json)?|```$", "", testo.strip(), flags=re.MULTILINE)
    inizio, fine = testo.find("["), testo.rfind("]")
    if inizio == -1 or fine == -1:
        raise ValueError(f"nessun array JSON nella risposta: {testo[:200]}")
    return json.loads(testo[inizio:fine + 1])

def punteggia_blocco(titoli):
    prompt = (
        "Assegna a ogni titolo un punteggio di rilevanza da 1 a 10 "
        "per un professionista che lavora su automazione AI.\n"
        "Rispondi SOLO con un array JSON, un oggetto per titolo, "
        "nello stesso ordine, formato [{\"i\": 0, \"score\": 7}]\n\n"
        + json.dumps(titoli, ensure_ascii=False)
    )
    resp = client.messages.create(
        model=MODEL,
        max_tokens=4000,
        messages=[{"role": "user", "content": prompt}],
    )
    return estrai_json(resp.content[0].text)

def assegna_punteggio(voci):
    out = []
    for offset in range(0, len(voci), BATCH):
        blocco = voci[offset:offset + BATCH]
        for p in punteggia_blocco([v["titolo"] for v in blocco]):
            i = p.get("i")
            if not isinstance(i, int) or not 0 <= i < len(blocco):
                continue
            out.append({"titolo": blocco[i]["titolo"], "score": p["score"]})
    return out

def main():
    voci = raccogli_titoli()
    if not voci:
        print("nessuna voce raccolta, esco senza errore")
        return 0
    punteggi = assegna_punteggio(voci)
    rilevanti = [p for p in punteggi if p["score"] > SOGLIA]
    print(f"raccolte {len(voci)} voci, {len(rilevanti)} sopra soglia {SOGLIA}")
    for r in rilevanti:
        print(f"[{r['score']}] {r['titolo']}")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Quattro scelte in questo codice contano più di quanto sembri:

  • La API key arriva da variabile d'ambiente, mai dal codice. os.environ["ANTHROPIC_API_KEY"] con le parentesi quadre e non .get(): se manca, lo script muore subito con un errore chiaro invece di arrivare alla chiamata API e fallire con un 401 criptico.
  • Il nome del modello è configurabile. I nomi dei modelli cambiano, e un identificativo hardcoded dentro l'immagine significa ricostruire e rideployare per cambiare una stringa. La lista aggiornata sta nella documentazione Anthropic.
  • Il JSON del modello viene estratto, non dato per buono. json.loads(resp.content[0].text) diretto va in JSONDecodeError la prima volta che il modello premette una frase o incornicia la risposta in un fence markdown. La funzione estrai_json toglie i fence e ritaglia dal primo [ all'ultimo ]. Il ciclo scarta anche gli indici fuori range, perché un modello che sbaglia un indice non deve far esplodere il job.
  • Lo script ritorna un exit code esplicito, e il batching a 40 titoli tiene le risposte dentro max_tokens. Cloud Run considera fallito un task che esce con codice diverso da zero, e questo è ciò che alimenta i retry. Uno script che stampa un errore ma esce con 0 risulta "riuscito" e non viene mai ritentato: puoi collezionare settimane di esecuzioni verdi con zero output prodotto.

Prima di andare avanti, si prova:

export ANTHROPIC_API_KEY="sk-ant-..."
python3 script.py

Se non funziona qui, non funzionerà nemmeno lì.

Step 2: containerizzare con Docker

Il Dockerfile per uno script Python sta in nove istruzioni:

FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY script.py .

RUN useradd --create-home appuser
USER appuser

CMD ["python", "script.py"]

Perché ogni riga sta lì:

  • `python:3.11-slim` invece di `python:3.11`. L'immagine completa porta con sé una toolchain di compilazione che non userai mai. La differenza è nell'ordine delle centinaia di MB, e su un'immagine che ricostruisci spesso si sente sui tempi di build e di pull.
  • `ENV PYTHONUNBUFFERED=1`. Questa riga sembra cosmetica e non lo è. Quando stdout non è un terminale, Python bufferizza l'output: i tuoi print() restano nel buffer e arrivano a Cloud Logging in ritardo, oppure spariscono del tutto se il task viene ucciso dal timeout prima del flush. Passi due sezioni a spiegare come si leggono i log e poi non ne vedi metà. Una riga chiude il problema.
  • `requirements.txt` copiato prima del codice. Sfrutta la cache dei layer Docker. Se cambi solo script.py e non le dipendenze, il layer del pip install viene riusato e il build dura secondi invece di minuti. Invertire queste due righe è l'errore di ottimizzazione più comune che mi capita di vedere.
  • `--no-cache-dir`. Evita che pip lasci l'archivio dei pacchetti scaricati dentro l'immagine finale. Peso morto.
  • L'utente non-root. Il container gira comunque isolato, ma se una dipendenza compromessa esegue codice, farlo senza privilegi di root è il minimo sindacale. Costa due righe.

Il .dockerignore evita di spedire nell'immagine roba che non deve starci:

.env
.git
__pycache__/
*.pyc
venv/
.venv/

La prima riga è quella che conta. Senza .dockerignore, un COPY . . distratto si porta dentro l'immagine il file con la tua API key, e quell'immagine resta nel registry a disposizione di chiunque abbia accesso in lettura al progetto.

Test locale dell'immagine:

docker build -t news-scout .
docker run --rm -e ANTHROPIC_API_KEY="sk-ant-..." news-scout

Se questo comando produce l'output atteso, il deploy è quasi garantito. Se fallisce qui, hai appena risparmiato dieci minuti di ciclo build-push-esegui-leggi-i-log sul cloud.

Step 3: build e push su Artifact Registry

Molti tutorial in circolazione mostrano ancora gcloud builds submit --tag gcr.io/PROJECT_ID/nome. Container Registry, il servizio dietro gcr.io, è stato spento: "Effective March 18, 2025, Container Registry is shut down and writing images to Container Registry is unavailable".

Una sfumatura che conviene conoscere, perché evita panico inutile: la stessa pagina precisa che "gcr.io URLs hosted on Artifact Registry, including Google-owned images with gcr.io URLs, are not affected by the Container Registry shutdown". Se in un Dockerfile vedi un FROM che punta a gcr.io, non è necessariamente rotto. Quello che non puoi più fare è scrivere immagini nel vecchio Container Registry. La migrazione è documentata nella pagina ufficiale sulla transizione.

Il repository si crea una volta sola per progetto:

gcloud artifacts repositories create automazioni \
  --repository-format=docker \
  --location=europe-west1 \
  --description="Immagini delle automazioni Claude"

Il formato del nome immagine su Artifact Registry è LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMMAGINE:TAG. Più lungo di gcr.io, ma esplicito su dove vive fisicamente il bit.

Il permesso che blocca il primo build su un progetto nuovo

Questo è il punto in cui, su un progetto appena creato, ti fermi davvero. Da un cambio distribuito tra maggio e giugno 2024, Cloud Build non usa più un service account dedicato: "After this change, Cloud Build now uses the Compute Engine default service account as the default service account".

La conseguenza pratica è che il service account con cui gira il build potrebbe non avere i permessi che servono, e la documentazione lo dice apertamente: se abiliti l'API dopo il cambio, devi "validate that the either the Compute Engine default service account or your user-created service account has enough permissions for your build", e chi lancia il build deve avere il permesso iam.serviceAccounts.actAs su quel service account.

Se il build si ferma con un errore di permessi, il rimedio documentato è concedere il ruolo Cloud Build Service Account al service account di default:

gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
  --role="roles/cloudbuild.builds.builder"

Il numero di progetto lo trovi con gcloud projects describe PROJECT_ID --format="value(projectNumber)". Se hai un'organizzazione sopra, il comportamento si governa con i vincoli di policy constraints/cloudbuild.useComputeServiceAccount e constraints/cloudbuild.useBuildServiceAccount, e la scelta la fa l'organizzazione, non il singolo progetto.

Fatto questo, il build gira sui server Google:

gcloud builds submit \
  --tag europe-west1-docker.pkg.dev/PROJECT_ID/automazioni/news-scout:latest

Cosa succede mentre guardi lo stream dei log: Cloud Build prende la directory corrente, la carica, costruisce l'immagine secondo il Dockerfile e la deposita nel repository.

Sul costo del build, un numero che gira sbagliato spesso: la quota gratuita è mensile, non giornaliera. "Each billing account comes with 2,500 free build-minutes per month". Un'immagine Python slim si costruisce in uno o due minuti, quindi la quota copre abbondantemente oltre mille build al mese. È una soglia a cui è difficile arrivare per sbaglio.

Una nota sui tag. Usare sempre :latest è comodo e funziona, ma toglie la possibilità di tornare indietro. Se un deploy rompe qualcosa alle 7:00 di un lunedì, un tag datato ti permette di ripuntare il job all'immagine di venerdì con un comando invece che con un rebuild. Il pattern che uso è taggare due volte, con latest e con la data.

Step 4: creare il Cloud Run Job

Con l'immagine nel registry, il job si crea così:

gcloud run jobs create news-scout \
  --image europe-west1-docker.pkg.dev/PROJECT_ID/automazioni/news-scout:latest \
  --region europe-west1 \
  --memory 512Mi \
  --cpu 1 \
  --task-timeout 300 \
  --max-retries 2

I flag, uno per uno, perché è qui che si decide il comportamento in produzione:

  • `--region europe-west1`. Belgio. Latenza bassa dall'Italia e, per chi tratta dati personali, il dato di esecuzione resta in UE. Serve anche al conto dei costi: europe-west1 sta in fascia Tier 1, quella con i prezzi più bassi. Se il tuo caso ha vincoli di residenza dei dati, questo flag non è una preferenza estetica.
  • `--cpu 1`. Attenzione, perché è una differenza reale tra Jobs e Services: i Job richiedono almeno 1 vCPU, e i valori ammessi sono 1, 2, 4, 6 e 8. Non puoi scendere a 0,5 vCPU come si fa sui Service.
  • `--memory 512Mi`. Con 1 vCPU la memoria configurabile va da 128 MiB a 4 GiB. Per uno script che fa parsing di RSS e chiamate HTTP, 512 MiB stanno larghi. Se il tuo script carica un CSV da mezzo giga in un DataFrame, il numero cambia.
  • `--task-timeout 300`. Cinque minuti. Oltre, il task viene interrotto. Il default è 10 minuti e il massimo arriva a 168 ore, quindi c'è spazio anche per batch lunghi. Impostarlo stretto è una rete di sicurezza, ma da solo non basta: se lo script fa una chiamata HTTP senza timeout applicativo, resta appeso finché non scade il timeout del task e poi viene ucciso a metà lavoro. I due timeout vanno impostati insieme, con quello applicativo più stretto.
  • `--max-retries 2`. Quante volte un task fallito viene ritentato, da 0 a 10. Due tentativi coprono il caso realistico del feed RSS temporaneamente irraggiungibile. Il rovescio è che con due retry un job che scrive su un file o manda una notifica può eseguire l'effetto collaterale tre volte: ogni automazione schedulata va scritta partendo dal presupposto che potrebbe girare due volte sullo stesso input.

Due flag che non ho messo nell'esempio ma che servono quando il lavoro cresce: --tasks divide l'esecuzione in più task paralleli (default 1, massimo 10.000) e --parallelism limita quanti girano insieme. Sono utili per elaborare una lista lunga a pezzi, inutili per uno script singolo.

Per eseguirlo subito e vedere se vive:

gcloud run jobs execute news-scout --region europe-west1

Se preferisci creare ed eseguire in un colpo solo, gcloud run jobs create accetta anche --execute-now.

Per leggere i log c'è un comando dedicato, in GA, che è quello da usare:

gcloud run jobs logs read news-scout \
  --region europe-west1 \
  --limit 50

Accetta --log-filter per restringere, per esempio --log-filter="severity>=ERROR", e --freshness per la finestra temporale (default un giorno). Quando ti serve interrogare i log fuori dal perimetro di un singolo job, o incrociare risorse diverse, si scende a Cloud Logging:

gcloud logging read \
  'resource.type="cloud_run_job" AND resource.labels.job_name="news-scout"' \
  --limit 50 \
  --format="value(textPayload)"

Step 5: i secret, fatti nel modo giusto

Il modo veloce di passare la API key al job è questo:

--set-env-vars ANTHROPIC_API_KEY=sk-ant-...

Funziona, ed è accettabile per cinque minuti mentre stai provando. Il problema è che la chiave finisce nella configurazione del job, visibile a chiunque abbia accesso in lettura al progetto e stampata nell'output di gcloud run jobs describe.

Il modo corretto costa due comandi in più. Prima si crea il secret:

echo -n "sk-ant-..." | gcloud secrets create anthropic-key \
  --data-file=- \
  --replication-policy="automatic"

L'-n di echo non è un dettaglio: senza, ci finisce dentro un carattere di a capo e la chiave risulta invalida in modi che fanno perdere mezz'ora.

Poi si aggancia il secret al job:

gcloud run jobs update news-scout \
  --region europe-west1 \
  --set-secrets ANTHROPIC_API_KEY=anthropic-key:latest

La sintassi è NOME_VARIABILE=NOME_SECRET:VERSIONE. Usare :latest significa che ruotando la chiave non devi toccare il job, basta aggiungere una versione nuova al secret.

Perché il job possa leggerlo, il service account che esegue il job deve avere il ruolo roles/secretmanager.secretAccessor. Se salti questa concessione, il job parte, prova a montare il secret e fallisce all'avvio con un errore di permessi.

Sul costo: Secret Manager include gratis 6 versioni attive e 10.000 operazioni di accesso al mese. Un'automazione giornaliera fa 30 accessi al mese. Sei dentro di due ordini di grandezza.

Step 6: schedulare con Cloud Scheduler

Ultimo pezzo. Il job esiste e funziona a comando, ora deve partire da solo.

gcloud scheduler jobs create http news-scout-daily \
  --location europe-west1 \
  --schedule="0 5 * * *" \
  --uri="https://run.googleapis.com/v2/projects/PROJECT_ID/locations/europe-west1/jobs/news-scout:run" \
  --http-method POST \
  --time-zone="Europe/Rome" \
  --oauth-service-account-email PROJECT_NUMBER-compute@developer.gserviceaccount.com

I punti che meritano attenzione:

  • Il formato della URI. È l'endpoint v2 dell'API Run: https://run.googleapis.com/v2/projects/PROJECT-ID/locations/REGIONE/jobs/NOME-JOB:run. Circolano ancora esempi con il vecchio percorso v1/namespaces. I due punti prima di run non sono un refuso, fanno parte della sintassi dei metodi custom nelle API Google.
  • Il fuso orario. Il default di --time-zone è Etc/UTC, quindi 0 5 * * * senza altro significa le 7:00 italiane durante l'ora legale e le 6:00 durante quella solare. Mettere --time-zone="Europe/Rome" toglie l'aritmetica due volte l'anno. È il tipo di dettaglio che non rompe niente il giorno del deploy e sposta il job di un'ora a fine ottobre, quando nessuno sta guardando.
  • `--http-method POST`. Nell'esempio è esplicito per leggibilità, non perché serva: su gcloud 566.0.0 il default di questo flag è già post. Lo scrivo perché chi legge il comando capisca al volo che sta invocando un metodo, non leggendo una risorsa.
  • Il service account. Quello di default PROJECT_NUMBER-compute@developer.gserviceaccount.com funziona nella maggior parte dei progetti, ma deve avere il ruolo roles/run.invoker. Se lo scheduler risulta eseguito con successo e il job non parte mai, il permesso mancante è il primo posto dove guardare.

Un'ultima trappola che costa poco evitare: tieni scheduler e job nella stessa region. Job in europe-west1 e scheduler in us-central1 è una combinazione che sembra funzionare finché non guardi la latenza o la fattura del traffico in uscita.

Per verificare senza aspettare le 7:00 di domani:

gcloud scheduler jobs run news-scout-daily --location europe-west1

Il caso pratico: il news scanner quotidiano

L'esempio non è teorico. Lo script che ho descritto gira ogni mattina alle 7:00:

  • legge 24 feed RSS, fino a 20 voci per feed, quindi fino a 480 titoli
  • li manda a Claude Haiku in blocchi da 40, cioè una dozzina di chiamate, per un punteggio di rilevanza da 1 a 10
  • tiene le notizie sopra 7 e scarta il resto

Tempo di esecuzione: circa 90 secondi. Tempo che mi risparmia: stimo 45 minuti al giorno di scrolling tra fonti [stima, N=1: è il tempo che ci mettevo a mano prima, mai cronometrato].

Il punto interessante non è il risparmio di tempo. È che la qualità del filtro non dipende più dal fatto che io sia in giornata buona. Un filtro mediocre ma costante batte un filtro eccellente applicato quando capita.

Costi reali, con la tabella giusta in mano

La pagina dei prezzi di Cloud Run ha tabelle separate per Services e per Jobs, e i numeri non coincidono. Le cifre che si vedono citate più spesso (180.000 vCPU-secondi, 360.000 GiB-secondi, 2 milioni di richieste) sono quelle dei Services con fatturazione a richiesta. In una guida sui Job il terzo numero non vuole dire niente: un Job non serve richieste, quindi nella sua tabella una quota di richieste non esiste proprio.

Questi sono i numeri della tabella Jobs:

  • CPU: primi 240.000 vCPU-secondi gratis al mese, poi $0,000018 per vCPU-secondo in fascia Tier 1.
  • Memoria: primi 450.000 GiB-secondi gratis al mese, poi $0,000002 per GiB-secondo in fascia Tier 1.
  • Richieste: voce assente.

Configurazione dell'esempio: 1 vCPU, 512 MiB, circa 90 secondi di esecuzione, una volta al giorno per 30 giorni, in europe-west1, che sta in Tier 1.

Consumo mensile:

  • vCPU-secondi: 1 × 90 × 30 = 2.700
  • GiB-secondi: 0,5 × 90 × 30 = 1.350

Percentuale della quota gratuita consumata:

  • CPU: 2.700 / 240.000 = 1,125%
  • RAM: 1.350 / 450.000 = 0,30%

A listino, ignorando completamente il free tier:

  • 2.700 × 0,000018 = $0,0486
  • 1.350 × 0,000002 = $0,0027
  • Totale: $0,0513, cioè poco più di cinque centesimi di dollaro al mese.

Il vincolo che stringe per primo è la CPU: 240.000 diviso 2.700 fa 88,9. Ottantotto automazioni di questo identico profilo prima di uscire dal free tier di calcolo. La memoria, da sola, ne reggerebbe 333.

A ottantotto non ci arrivi, e il motivo non ha niente a che vedere col calcolo:

  • Cloud Scheduler: $0,10 per job al mese, con tre job gratuiti al mese contati per account di fatturazione, non per progetto. La documentazione fa l'esempio esplicito: cinque progetti con due job ciascuno danno 3 job gratis e 7 a pagamento. Anche un job in pausa continua a contare come job.
  • Cloud Build: 2.500 build-minuti gratis al mese per account di fatturazione, più che sufficienti.
  • Secret Manager: 6 versioni attive e 10.000 accessi al mese inclusi.
  • Cloud Logging: primi 50 GiB di ingestion al mese. Tre righe di stampa non ci arrivano mai. Uno script lasciato in debug che logga ogni risposta API completa ci arriva più in fretta di quanto sembri, ed è la voce che ho visto crescere di più senza un motivo vero.
  • Artifact Registry: lo storage delle immagini si paga a GiB, e i tag vecchi restano lì finché non li cancelli tu.

Il quarto job schedulato costa dieci centesimi al mese, cioè il doppio di tutto il calcolo dell'esempio. In assoluto è una cifra irrilevante. In proporzione dice una cosa precisa su questa architettura: quello che paghi non è il lavoro, è il diritto di farlo partire da solo.

E la conclusione più grande regge meglio con i numeri veri che con le stime: l'infrastruttura non è la voce di costo di un'automazione AI. Il costo è il modello. Ogni ora spesa a ottimizzare il container è spesa sulla voce sbagliata, perché la voce giusta è il consumo di token. Sui piani e sul costo reale dell'accesso a Claude ho scritto un confronto dedicato.

Dove finisce Cloud Run, e dove no

Sarebbe comodo chiudere dicendo che questo schema regge tutto il sistema che gestisco. Non è così, e il conto vero è più utile di quello comodo.

Al 27 luglio 2026, guardando cosa gira davvero:

  • 5 Cloud Run Job. Le due pipeline Instagram (reel e caroselli), la pipeline serale, il monitor di indicizzazione Search Console, il finisher del blog. Tutta roba che gira in container, senza che io sia davanti al Mac.
  • 20 task locali su Claude Cowork. Girano sul Mac e pretendono il Mac acceso: orchestratore settimanale, monitor SEO, consolidamento della memoria.
  • Una dozzina di Routine cloud. Agenti gestiti che girano nell'infrastruttura di Anthropic, senza container e senza Dockerfile. Diverse di queste sono proprio ciò che invoca i Cloud Run Job elencati sopra.
  • 10 LaunchAgent sul Mac. Script deterministici, zero LLM: sincronizzazione del bus di segnali, health check, backup, bonifica dei lock.

Quattro runtime diversi, con quattro profili di guasto diversi. La scelta tra loro segue quattro domande, in quest'ordine:

  1. Il task ha bisogno di qualcosa che sta solo sul mio disco? Filesystem locale, una sessione di browser già autenticata, materiale che non deve uscire dalla macchina. Se sì, resta locale. Questo criterio batte tutti gli altri, anche quando gli altri direbbero cloud.
  2. Cosa succede se salta per quattro ore? Se la risposta è "niente di grave", il Mac va benissimo. Se salta qualcosa con una scadenza esterna, va in cloud e basta.
  3. Quanto dura un run? Sopra i cinque o sei minuti la forma "agente" diventa scomoda e la forma "Job" diventa naturale.
  4. Chi sorveglia chi? Un guardiano che gira sulla stessa macchina che sorveglia non è un guardiano. Gli health check stanno fuori dalla macchina che controllano.

Cloud Run Jobs vince quando il lavoro è deterministico, containerizzabile, e deve girare a macchina spenta. Perde quando serve il disco locale, un browser autenticato, o un agente conversazionale invece di un batch. Ho raccontato come convivono questi mondi nello stesso sistema in un caso studio sull'architettura ibrida.

FAQ

Posso usare Cloud Run senza scrivere un Dockerfile?

Sì. gcloud run jobs deploy accetta il flag --source, che punta a una directory locale: se dentro trova un Dockerfile lo usa, altrimenti costruisce l'immagine con i buildpack di Google. In pratica:

gcloud run jobs deploy news-scout \
  --source . \
  --region europe-west1 \
  --task-timeout 300

I flag --source e --image sono mutuamente esclusivi, quindi scegli una strada e restaci. Il compromesso è il solito: parti prima, controlli meno. Con i buildpack non decidi tu l'immagine base, la versione di Python o l'utente con cui gira il processo, e nemmeno il PYTHONUNBUFFERED di cui sopra. Per iniziare va bene. Quando l'automazione diventa qualcosa da cui dipendi, il Dockerfile esplicito ripaga.

Conviene spostare tutto sul cloud?

No, e questa è la risposta che mi costa più discussioni. Alcune automazioni hanno bisogno di stare sulla macchina locale: quelle che toccano file nel tuo filesystem, quelle che usano credenziali di sessione di applicazioni desktop, quelle che elaborano materiale che non deve uscire dal tuo disco. La domanda vera non è "locale o cloud", è "questo task ha bisogno che io sia presente". Se la risposta è no, allora è candidato al cloud.

Il riassunto operativo

Il percorso completo di questa guida sta in una dozzina di comandi gcloud e un Dockerfile di nove istruzioni. La seconda volta che lo fai ne servono cinque: abilitare le API, creare il repository, lanciare il build, creare il job, creare la schedulazione.

Per il livello sotto, cioè come si scrivono gli script che poi finiscono qui dentro, la guida completa a Claude Code copre gli strumenti. Per il livello sopra, quali processi meritino un job schedulato e quali andrebbero solo cancellati, il ragionamento sta nella guida all'automazione AI per il B2B.

Riferimento tecnico completo e sempre aggiornato: documentazione ufficiale di Cloud Run.

Condividi

— Newsletter LinkedIn

Ogni settimana condivido workflow, errori e numeri reali

35 automazioni in produzione, zero dipendenti. Su LinkedIn documento il dietro le quinte: cosa funziona, cosa no, e i dati che nessuno mostra.