How-to
Automating Reporting WorkflowsDeep dive

Run an Excel Report on Google Cloud Run Jobs

Package a Python Excel report as a container and run it on a schedule with Cloud Run jobs and Cloud Scheduler: a slim image, a script that exits with a meaningful status, parallel tasks that split work by CLOUD_RUN_TASK_INDEX, retries and timeouts, a service account instead of keys, and a local test with Docker before anything is deployed.

Cloud Run jobs are Google Cloud's way to run a container to completion: start it, let it do its work, and stop — exactly the shape of a report script. Unlike Cloud Run services, a job listens on no port, can run for hours, and can fan out into parallel tasks. Cloud Scheduler triggers it on a cron schedule. Everything the report needs — pandas, openpyxl, even LibreOffice for PDF export — goes into the image, so there are no runtime limits on dependencies of the kind functions impose. This page, part of Scheduling Python Excel Scripts with Cron, builds the container, runs it locally with Docker in exactly the way Cloud Run will, and then deploys and schedules it.

Prerequisites

Bash
pip install pandas openpyxl        # for editing the script locally
# Docker for the local test; the gcloud CLI to deploy

The report writes one workbook per sales region. On Cloud Run the workbooks would be uploaded to Cloud Storage; to keep the local test self-contained, the script writes them to a folder inside the container and the test lists them. Upload an Excel Report to Google Cloud Storage covers the upload step.

From a script to a scheduled job The report script is packaged in a container image, pushed to Artifact Registry, deployed as a Cloud Run job with retries and a timeout, and started on a cron schedule by Cloud Scheduler; each execution runs one or more tasks. Script exits 0 or 1 Image Dockerfile Cloud Run job tasks, retries, timeout Cloud Scheduler cron trigger Execution parallel tasks

Step 1 — A script that knows which part is its job

A job can run several tasks in parallel. Each task receives its position in CLOUD_RUN_TASK_INDEX and the total in CLOUD_RUN_TASK_COUNT, so the script can take its share of the work — here, every count-th region starting at its index. With one task, it does everything:

Python
import os
import sys
from pathlib import Path

import pandas as pd

REGIONS = ["North", "South", "East", "West"]

def main() -> int:
    index = int(os.environ.get("CLOUD_RUN_TASK_INDEX", 0))
    count = int(os.environ.get("CLOUD_RUN_TASK_COUNT", 1))
    attempt = int(os.environ.get("CLOUD_RUN_TASK_ATTEMPT", 0))
    mine = REGIONS[index::count]
    out = Path(os.environ.get("OUTPUT_DIR", "/tmp/reports"))
    out.mkdir(parents=True, exist_ok=True)
    for region in mine:
        df = pd.DataFrame({"Region": [region], "Sales": [100_000 + 1_000 * REGIONS.index(region)]})
        df.to_excel(out / f"weekly_sales_{region}.xlsx", index=False)
    print(f"task {index + 1}/{count} (attempt {attempt}) wrote {mine}", flush=True)
    return 0

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

REGIONS[index::count] is a simple, stable way to split work: with two tasks, task 0 takes North and East and task 1 takes South and West, and no region is done twice or missed. CLOUD_RUN_TASK_ATTEMPT counts retries of this task, useful in logs. The exit status matters more than it looks: Cloud Run decides whether a task succeeded from the process exit code, so a script that catches every exception and exits 0 turns failures into green runs. Let errors propagate, or return 1.

Step 2 — Package it

A slim Python base image, the dependencies pinned in requirements.txt, and the script. Running as a non-root user is good practice and costs nothing:

Text
pandas==3.0.6
openpyxl==3.1.5
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
USER nobody
CMD ["python", "main.py"]
Bash
docker build -q -t weekly-sales-job . > /dev/null && echo "image built"
docker image inspect weekly-sales-job --format '{{.Config.User}} {{.Config.Cmd}}'

Copying requirements.txt and installing before copying the script means Docker reuses the dependency layer when only the script changes, so rebuilds take seconds. Pinning exact versions keeps next month's build from quietly picking up a pandas release that changes an output.

Step 3 — Test the job locally, the way Cloud Run runs it

Cloud Run runs the image's command with the task environment variables set. docker run can do the same, which tests the container — not just the script — before anything is deployed. Run it once as a single task, then as the two tasks of a parallel execution:

Bash
docker run --rm weekly-sales-job sh -c 'python main.py && ls /tmp/reports'
echo "exit status: $?"
for i in 0 1; do
  docker run --rm -e CLOUD_RUN_TASK_INDEX=$i -e CLOUD_RUN_TASK_COUNT=2 weekly-sales-job sh -c 'python main.py && ls /tmp/reports'
done

As one task, the container writes all four workbooks and exits with status 0. As two tasks, the regions split cleanly — North and East in the first, South and West in the second — and each task runs in its own container with its own file system, which is why the output must go to shared storage such as Cloud Storage rather than a local folder. A failing script would show a non-zero status here, exactly as Cloud Run would record it.

One execution, two parallel tasks A job execution with a task count of two fans out to two containers. Task 0 builds the North and East workbooks and task 1 builds South and West, each uploading to Cloud Storage. Execution --tasks 2 Task 0 — North, East CLOUD_RUN_TASK_INDEX=0 Task 1 — South, West CLOUD_RUN_TASK_INDEX=1

Step 4 — Deploy the job

Build the image in Cloud Build, then create the job with its retry and timeout settings and a dedicated service account:

Bash
gcloud builds submit --tag europe-west2-docker.pkg.dev/acme-analytics/reports/weekly-sales-job
gcloud run jobs create weekly-sales-job \
  --image europe-west2-docker.pkg.dev/acme-analytics/reports/weekly-sales-job \
  --region europe-west2 \
  --tasks 2 --parallelism 2 \
  --max-retries 2 --task-timeout 20m \
  --memory 1Gi \
  --service-account weekly-reports@acme-analytics.iam.gserviceaccount.com
gcloud run jobs execute weekly-sales-job --region europe-west2 --wait

--max-retries retries a failed task — not the whole job — so a transient database error in one region reruns only that region. --task-timeout stops a stuck task; set it to a few times the normal run time rather than the maximum. The service account carries the job's permissions — read the source, write the report bucket — so no key files are needed inside the image; see Store Credentials for Excel Report Scripts Safely. execute --wait runs it once and waits, which is the deployment smoke test.

Step 5 — Schedule it

Cloud Scheduler starts the job by calling the Cloud Run Admin API. Create a scheduler job with a cron expression, a time zone and a service account allowed to run the job:

Bash
gcloud scheduler jobs create http weekly-sales-monday \
  --location europe-west2 \
  --schedule "0 7 * * 1" --time-zone "Europe/London" \
  --uri "https://run.googleapis.com/v2/projects/acme-analytics/locations/europe-west2/jobs/weekly-sales-job:run" \
  --http-method POST \
  --oauth-service-account-email scheduler-invoker@acme-analytics.iam.gserviceaccount.com

Unlike Azure's six-field format, Cloud Scheduler uses standard five-field cron, and --time-zone makes 07:00 mean 07:00 London time all year, through the clock changes. The console's Triggers tab on the job page creates the same scheduler job with a form. Cloud Logging collects each task's output — the print line from Step 1 — and a log-based alert on failed executions closes the loop: Detect a Scheduled Report That Did Not Run covers the other failure, a run that never started.

Monday 07:00, London time A sequence diagram. Cloud Scheduler calls the Cloud Run Admin API to run the job; Cloud Run starts two task containers, which build and upload their workbooks and exit with status zero; the execution is marked succeeded. Cloud Scheduler Cloud Run task containers 1. POST …/jobs/weekly-sales-job:run 2. start 2 tasks with index 0 and 1 3. exit 0 → execution succeeded

Performance notes

A job is billed for the CPU and memory its tasks use while they run, so a weekly report that takes two minutes costs very little; parallel tasks finish sooner but do not cost less in total. Start-up adds a few seconds per task to pull the image and start Python — keep images slim and put heavy imports only where they are needed. Splitting work into tasks pays off when each unit takes minutes, as a large regional extract does; for four small workbooks, one task is simpler.

Common pitfalls

  • Swallowing exceptions. Cloud Run judges success by exit code; a script that exits 0 after an error reports success.
  • Writing output to the container's disk. It disappears when the task ends; upload to Cloud Storage.
  • Uneven work splits. Splitting by index works when units are similar in size; for very uneven work, give big units their own task.
  • Retries that duplicate output. A retried task runs again from the start; make uploads idempotent, for example by overwriting the same object name.
  • Timeouts set to the maximum. A stuck task then burns hours; set a timeout a few times the normal duration.

Conclusion

Write the report as a script that exits non-zero on failure and takes its share of the work from CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT, package it in a slim image with pinned dependencies, and test the container locally with docker run and the same environment variables. Deploy it as a Cloud Run job with task retries, a sensible timeout and its own service account, and start it with a Cloud Scheduler job on a five-field cron schedule in your time zone.

Frequently asked questions

What is the difference between a Cloud Run service and a Cloud Run job? A service runs a web server that answers requests; a job runs a container to completion and stops. Reports, exports and batch scripts are jobs.

How do I schedule a Cloud Run job? Create a Cloud Scheduler job that sends a POST to the job's run endpoint in the Cloud Run Admin API, with a cron schedule, a time zone and a service account that is allowed to run the job. The job page's Triggers tab creates it for you.

How do I run parts of a report in parallel on Cloud Run? Set --tasks and --parallelism on the job and have the script read CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT to pick its share of the work, for example every count-th region starting at its index.

How long can a Cloud Run job run? Each task has a timeout you set with --task-timeout, which can be much longer than function limits — hours rather than minutes — so long reports that do not fit in a function fit comfortably in a job.

More on scheduling & orchestration

More on scheduling & orchestration →