Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added gcp/__init__.py
Empty file.
Empty file added gcp/cloud_run/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions gcp/cloud_run/id/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*
!Dockerfile
!pyproject.toml
!__init__.py
!activities.py
!settings.py
!worker.py
!workflows.py
49 changes: 49 additions & 0 deletions gcp/cloud_run/id/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# syntax=docker/dockerfile:1

# The git-pinned temporalio compiles the Rust core; drop this builder for a Python base once the plugin ships on PyPI.
FROM rust:1.91.0-slim-bookworm AS builder

COPY --from=ghcr.io/astral-sh/uv:0.8.15 /uv /uvx /bin/

RUN apt-get update \
&& apt-get install --no-install-recommends --yes \
build-essential \
ca-certificates \
git \
libprotobuf-dev \
pkg-config \
protobuf-compiler \
python3 \
python3-dev \
&& rm -rf /var/lib/apt/lists/*

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON=/usr/bin/python3

WORKDIR /app
COPY pyproject.toml ./
RUN uv sync --no-dev

FROM debian:bookworm-slim

ENV PATH=/app/.venv/bin:$PATH \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

RUN apt-get update \
&& apt-get install --no-install-recommends --yes ca-certificates python3 \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system app \
&& useradd --system --gid app --create-home app

WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
# The build context is this sample directory (flat), so recreate the
# gcp/cloud_run/id package path and its parent package markers.
COPY --chown=app:app __init__.py activities.py settings.py worker.py workflows.py /app/gcp/cloud_run/id/
RUN touch /app/gcp/__init__.py /app/gcp/cloud_run/__init__.py \
&& chown -R app:app /app/gcp

USER app
CMD ["python", "-m", "gcp.cloud_run.id.worker"]
62 changes: 62 additions & 0 deletions gcp/cloud_run/id/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Google Cloud Run Id

Run a Temporal Worker in a [Google Cloud Run worker
pool](https://cloud.google.com/run/docs/worker-pools) and derive its identity
from Cloud Run instance metadata with
[`temporalio.contrib.gcp.cloud_run.id`](https://python.temporal.io/temporalio.contrib.gcp.cloud_run.id.html)'s
`CloudRunIdPlugin`. Registered on the client, the plugin sets the identity to
`<instance_id>@<revision>` at connect time and propagates it to the worker, so
each running container is individually identifiable as a poller.

`CloudRunIdPlugin` is unreleased, so `pyproject.toml` pins `temporalio` to the
SDK commit that adds it (a git source, which also builds inside the container);
drop the `[tool.uv.sources]` override once it ships on PyPI.

Configure the worker and starter with `TEMPORAL_TASK_QUEUE` (required),
`TEMPORAL_ADDRESS` (default `localhost:7233`), `TEMPORAL_NAMESPACE` (default
`default`), and `TEMPORAL_API_KEY` (set for Temporal Cloud; enables TLS).
Cloud Run sets `CLOUD_RUN_WORKER_POOL` and `CLOUD_RUN_REVISION` automatically.
Worker pools bill while running, so scale to zero (step 4) after testing.

## 1. Deploy the worker pool

Cloud Build builds the image from the `Dockerfile` and starts one instance:

```bash
gcloud run worker-pools deploy temporal-worker --source . --region us-central1 \
--set-env-vars TEMPORAL_ADDRESS=your-namespace.account-id.tmprl.cloud:7233,TEMPORAL_NAMESPACE=your-namespace.account-id,TEMPORAL_TASK_QUEUE=gcp-cloud-run \
--set-secrets TEMPORAL_API_KEY=temporal-api-key:latest
```

Omit `--set-secrets` for a plaintext self-hosted server.

## 2. Confirm the worker registered

The startup log reports the derived identity `<instance_id>@<revision>`:

```bash
gcloud run worker-pools logs read temporal-worker --region us-central1 --limit 50
```

## 3. Start a Workflow

Run the starter locally from the repository root, against the same service and
task queue:

```bash
TEMPORAL_ADDRESS=your-namespace.account-id.tmprl.cloud:7233 \
TEMPORAL_NAMESPACE=your-namespace.account-id \
TEMPORAL_TASK_QUEUE=gcp-cloud-run \
TEMPORAL_API_KEY="$(cat /secure/path/to/temporal-api-key)" \
uv run python -m gcp.cloud_run.id.starter
```

It prints `Workflow result: Hello, Cloud Run worker pool!`.

## 4. Scale to zero

```bash
gcloud run worker-pools update temporal-worker --instances 0 --region us-central1
```

Cloud Run sends `SIGTERM` and the worker shuts down gracefully.
1 change: 1 addition & 0 deletions gcp/cloud_run/id/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Google Cloud Run Id sample."""
11 changes: 11 additions & 0 deletions gcp/cloud_run/id/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Activity used by the Cloud Run worker sample."""

from __future__ import annotations

from temporalio import activity


@activity.defn
async def compose_greeting(name: str) -> str:
activity.logger.info("Composing greeting for %s", name)
return f"Hello, {name}!"
22 changes: 22 additions & 0 deletions gcp/cloud_run/id/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[project]
name = "temporalio-samples-gcp-cloud-run-id"
version = "0.1a1"
description = "Cloud Run instance identity for a Temporal Worker on Google Cloud Run"
authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }]
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
dependencies = ["temporalio>=1.33.0,<2"]

[dependency-groups]
dev = [
"ruff>=0.5.0,<0.6",
"mypy>=1.4.1,<2",
]

[tool.uv]
package = false

# CloudRunIdPlugin is unreleased; pin temporalio to the SDK commit that adds it until it ships on PyPI.
[tool.uv.sources]
temporalio = { git = "https://github.com/temporalio/sdk-python", rev = "fa5cf46ea04920be138bafca7e20ca6208822c30" }
42 changes: 42 additions & 0 deletions gcp/cloud_run/id/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Temporal connection settings shared by the worker and the starter.

Values are read from the environment so the same code runs against a local
plaintext dev server or Temporal Cloud. Set ``TEMPORAL_API_KEY`` to connect to
Temporal Cloud (which enables TLS); leave it unset for a plaintext connection.
"""

from __future__ import annotations

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
address: str
namespace: str
task_queue: str
api_key: str | None

@property
def tls(self) -> bool:
# Temporal Cloud requires TLS; a plaintext self-hosted server does not.
return self.api_key is not None


def load_settings() -> Settings:
"""Build connection settings from TEMPORAL_* environment variables."""
task_queue = os.environ.get("TEMPORAL_TASK_QUEUE")
if not task_queue:
raise RuntimeError("TEMPORAL_TASK_QUEUE must be set to a non-empty value")

# Secret managers frequently preserve a trailing newline; strip it.
api_key = os.environ.get("TEMPORAL_API_KEY")
api_key = api_key.strip() if api_key else None

return Settings(
address=os.environ.get("TEMPORAL_ADDRESS") or "localhost:7233",
namespace=os.environ.get("TEMPORAL_NAMESPACE") or "default",
task_queue=task_queue,
api_key=api_key or None,
)
36 changes: 36 additions & 0 deletions gcp/cloud_run/id/starter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Start a GreetingWorkflow on the Cloud Run worker's task queue.

Run this locally against the same Temporal service the worker connects to, using
the same TEMPORAL_* environment variables.
"""

from __future__ import annotations

import asyncio

from temporalio.client import Client

from gcp.cloud_run.id.settings import load_settings
from gcp.cloud_run.id.workflows import GreetingWorkflow


async def main() -> None:
settings = load_settings()
client = await Client.connect(
settings.address,
namespace=settings.namespace,
api_key=settings.api_key,
tls=settings.tls,
)

result = await client.execute_workflow(
GreetingWorkflow.run,
"Cloud Run worker pool",
id="gcp-cloud-run-id-sample",
task_queue=settings.task_queue,
)
print(f"Workflow result: {result}")


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading