Cloud MLOps

Declarative Cloud Run: GitOps Without a Cluster

How I structured a Cloud Run config repo — defining every AI service, job, and event trigger as Knative YAML and deploying through a Git-merge-driven Jenkins pipeline.

11 min read
One App, One Directory

A Cloud Run config repo works best with one rule: each subdirectory represents one application. Inside you find service.yaml (for long-running services), job.yaml (for batch workloads), and optionally trigger.yaml (for Eventarc bindings). Every service in the fleet — the Knowledge Base retrieval API, AI agents, background processing jobs, event-driven Cloud Functions — is declared in these files. No imperative CLI commands live in anyone's head; everything is in Git. This provides environment parity across DEV, UAT, and PROD.

yaml
# my-ai-service/service.yaml — Production configuration
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: my-ai-service
  annotations:
    run.googleapis.com/minScale: "1"
    run.googleapis.com/maxScale: "5"
    run.googleapis.com/ingress: internal
    run.googleapis.com/default-url-disabled: "true"
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/cloudsql-instances: my-project:asia-southeast2:my-db
        run.googleapis.com/vpc-access-egress: all-traffic
        run.googleapis.com/startup-cpu-boost: "true"
    spec:
      containerConcurrency: 60
      timeoutSeconds: 300
      containers:
        - name: my-ai-service
          image: asia-southeast2-docker.pkg.dev/my-project/my-repo/my-ai-service:1.0.0-prod
          command: ["/cnb/lifecycle/launcher"]
          args: ["gunicorn", "app.main:app", "-w", "2",
                 "-k", "uvicorn.workers.UvicornWorker",
                 "-b", "0.0.0.0:8080"]
Eventarc Triggers and Pub/Sub Schemas

Event-driven Cloud Functions use trigger.yaml to bind to GCS bucket finalizations or Pub/Sub topics. Our deploy pipeline automatically provisions Pub/Sub schemas from co-located JSON files, ensuring message contracts are enforced at the infrastructure level. Since GCP schemas are immutable, we version them (my-schema-v2.json) and update the trigger reference — the pipeline handles creation and attachment automatically. This prevents data formatting bugs from crashing our downstream consumer services.

yaml
# gcs-event-processor/trigger.yaml
name: gcs-event-processor-trigger
eventFilters:
- attribute: type
  value: google.cloud.storage.object.v1.finalized
- attribute: bucket
  value: my-data-landing-zone
destination:
  cloudRunService:
    service: gcs-event-processor
serviceAccount: data-processor@my-project.iam.gserviceaccount.com
transport:
  pubsub:
    topic: data-ingestion-topic
    schema: ingestion-schema-v2
Deployment Flow via Jenkins Reactor

When a PR is merged into the config repo, a Git webhook triggers the Jenkins deploy pipeline. The pipeline checks out the repository, runs git diff to identify which YAML files changed, and executes the corresponding gcloud run services replace or gcloud run jobs replace commands. This "reactor" pattern means the pipeline accepts all merge events and intelligently decides what to deploy based on the diff. Unchanged services are left completely untouched, minimizing risk and speeding up build cycles.

Cloud Run Deployment Flow
sequenceDiagram participant AppRepo as Source Code Repo participant BuildJenkins as Jenkins Build participant ConfigRepo as CloudRun Config Repo participant DeployJenkins as Jenkins Deploy participant CloudRun as GCP Cloud Run AppRepo->>BuildJenkins: Git Push Tag BuildJenkins->>ConfigRepo: Create PR (update image tag) ConfigRepo->>DeployJenkins: Merge Trigger (webhook) DeployJenkins->>CloudRun: gcloud run services replace
Lessons Learned: Config Parity Gotchas

While Cloud Run abstracts away cluster management, it enforces strict container constraints. One notable gotcha is network connectivity. If your Cloud Run service needs to reach internal VPC resources (like Elasticsearch or SQL), you must route traffic via Serverless VPC Access Connectors or direct VPC routing. If the connector is under-provisioned, you will experience random request timeouts and connection drops under load. I learned the hard way that load testing your network connectors is just as important as load testing your application code.

Serverless GitOps is about reaping the benefits of declarative pipelines without paying the complexity tax of maintaining an entire Kubernetes cluster.

More Recent Posts