Run an Excel Report on Google Cloud Run Jobs
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
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.
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:
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:
pandas==3.0.6
openpyxl==3.1.5
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"]
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:
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.
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:
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:
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.
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.
Related
- Up one level: Scheduling Python Excel Scripts with Cron — every way to run reports on a schedule.
- Run a Python Excel Report in Docker — the container basics this page builds on.
- Run an Excel Report as a Kubernetes CronJob — the same container on Kubernetes.
- Run an Excel Report in Azure Functions — the serverless option for short reports.
More on scheduling & orchestration
- How-toDownload New Excel Files from an SFTP Server
- How-toRead Excel Attachments into a Database Nightly
- How-toRefresh an Excel Report from a Database on a Schedule
- How-toRun a Report When an Excel File Changes in OneDrive
- How-toSend Report Emails at Each Recipient's Local Time
- How-toDetect a Scheduled Report That Did Not Run