Skip to main content

Deploy a Serverless Worker on GCP Cloud Run

View Markdown

This guide walks through deploying a Temporal Serverless Worker to a GCP Cloud Run Worker Pool.

A Cloud Run Worker Pool runs long-lived instances that poll the Task Queue continuously. Temporal's Worker Controller Instance (WCI) adjusts the pool's instance count through the Cloud Run admin API as work arrives and drains. For how the pool scales and how instances are shut down, see Serverless Workers on GCP Cloud Run.

Prerequisites

  • A Temporal Cloud account with a GCP-hosted Namespace, or a self-hosted Temporal Service v1.31.0 or later. The Namespace's cloud provider must match the serverless compute provider.
  • For self-hosted deployments, complete the self-hosted setup before following this guide.
  • Every Workflow must declare a versioning behavior, or the Worker must set a default versioning behavior.
  • A GCP project with the Cloud Run and Artifact Registry APIs enabled, and permissions to create Worker Pools, service accounts, and Secret Manager secrets.
  • The gcloud CLI installed and authenticated. You may use other tools to perform the GCP steps, such as the Google Cloud console or Terraform.
  • Terraform installed. Temporal provides the IAM setup as a Terraform module.
  • The Go SDK, Python SDK, or TypeScript SDK, depending on which language you are using. Use the tabs to select your language and the rest of the page will update accordingly.

1. Write Worker code

A Cloud Run Serverless Worker is a standard long-running Worker. It connects to Temporal, registers its Workflows and Activities, declares its Worker Deployment Version, and polls the Task Queue until the instance is shut down.

package main

import (
"log"
"os"

"go.temporal.io/sdk/client"
"go.temporal.io/sdk/contrib/envconfig"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"

"example.com/myapp"
)

func main() {
c, err := client.Dial(envconfig.MustLoadDefaultClientOptions())
if err != nil {
log.Fatalln("Unable to create client", err)
}
defer c.Close()

w := worker.New(c, os.Getenv("TEMPORAL_TASK_QUEUE"), worker.Options{
DeploymentOptions: worker.DeploymentOptions{
UseVersioning: true,
Version: worker.WorkerDeploymentVersion{
DeploymentName: "my-app",
BuildID: "build-1",
},
},
})

w.RegisterWorkflowWithOptions(myapp.MyWorkflow, workflow.RegisterOptions{
VersioningBehavior: workflow.VersioningBehaviorPinned,
})
w.RegisterActivity(myapp.MyActivity)

if err := w.Run(worker.InterruptCh()); err != nil {
log.Fatalln("Unable to start worker", err)
}
}

Each Workflow must have a versioning behavior, either VersioningBehaviorPinned or VersioningBehaviorAutoUpgrade. Set it per Workflow at registration as shown above, or set a Worker-level default in DeploymentOptions:

w := worker.New(c, os.Getenv("TEMPORAL_TASK_QUEUE"), worker.Options{
DeploymentOptions: worker.DeploymentOptions{
UseVersioning: true,
Version: version,
DefaultVersioningBehavior: workflow.VersioningBehaviorPinned,
},
})

If a Version is set and neither is specified, registration panics with workflow type does not have a versioning behavior.

To export traces and Temporal Core metrics to Google Cloud, add the OpenTelemetry plugin to the Worker. See Serverless Workers on GCP Cloud Run - Go SDK.

2. Deploy to a Cloud Run Worker Pool

Containerize the Worker, push the image to Artifact Registry, and create the Worker Pool.

i. Containerize the Worker

Package the Worker and its dependencies into a container image that Cloud Run runs for each Worker Pool instance. The image's entrypoint must start your Worker process, so an instance begins polling the Task Queue as soon as it starts.

Build a static binary in one stage and copy it into a minimal runtime image:

FROM golang:1.25 AS build

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/worker ./worker

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/worker /worker
CMD ["/worker"]

CGO_ENABLED=0 produces a statically linked binary, which is what the distroless/static base image expects.

ii. Build and push the image

Build the image and push it to Artifact Registry:

gcloud builds submit \
--tag <REGION>-docker.pkg.dev/<YOUR_GCP_PROJECT>/<REPOSITORY>/my-temporal-worker:build-1 \
--project <YOUR_GCP_PROJECT> \
--region <REGION>

iii. Create the Worker Pool

Create an empty Worker Pool. Start it at zero instances: the WCI raises the instance count once the Worker Deployment Version is current and Tasks arrive.

Create one Worker Pool per Worker Deployment Version. A pool runs a single container image, and a Worker Deployment Version pins a single build, so each new build needs its own pool. Include the build ID in the pool name to keep that mapping visible.

Choose a runner service account

The Worker Pool runs as a runner service account, the runtime identity your Worker code uses to reach other Google Cloud services. It is not the invoker service account from Step 3, which Temporal impersonates to scale the pool but never runs it.

Use a dedicated service account for the pool, and create one if you do not already have a suitable account. If you omit --service-account, Cloud Run falls back to the default Compute Engine service account, which holds roles/editor. You need the email twice: here, and in Step 3 as runner_service_account_email.

Store the Temporal Cloud API key (or TLS material) in Secret Manager rather than passing it as a plaintext environment variable.

The runner service account needs no baseline role to run the Worker. Cloud Run collects stdout and stderr into Cloud Logging through its own infrastructure, and the Cloud Run service agent, not the runner service account, pulls the container image. Grant the runner service account only what your code reaches:

  • roles/secretmanager.secretAccessor on each secret you mount, including the API key above.
  • roles/logging.logWriter, only if your Worker writes through the Cloud Logging API instead of stdout and stderr.
  • Access to any other Google Cloud service your Workflows and Activities call.
gcloud run worker-pools deploy my-temporal-worker-pool-build-1 \
--image <REGION>-docker.pkg.dev/<YOUR_GCP_PROJECT>/<REPOSITORY>/my-temporal-worker:build-1 \
--region <REGION> \
--project <YOUR_GCP_PROJECT> \
--service-account <RUNNER_SERVICE_ACCOUNT> \
--instances 0 \
--set-env-vars TEMPORAL_ADDRESS=<your-temporal-address>:7233,TEMPORAL_NAMESPACE=<your-namespace>,TEMPORAL_TASK_QUEUE=my-task-queue \
--set-secrets TEMPORAL_API_KEY=<SECRET_NAME>:latest
ParameterDescription
--imageThe Worker image you pushed in Step ii.
--service-accountThe runner service account the Worker Pool instances run as. It needs read access to the secrets referenced below. This is distinct from the invoker service account in Step 3, which Temporal impersonates to scale the pool.
--instancesInitial instance count. Set to 0; the WCI manages the count after the version is current.
--set-env-varsNon-secret Worker configuration. See Environment configuration.
--set-secretsMaps a Secret Manager secret to an environment variable. Use for TEMPORAL_API_KEY or TLS client cert/key material.

The environment configuration package reads environment variables and configuration files at startup. For the full list of supported environment variables, config file format, and profiles, see Environment configuration.

3. Grant Temporal permission to manage the Worker Pool (Cloud only)

This section applies to Temporal Cloud. For self-hosted Temporal Service deployments, see Self-hosted setup.

Temporal Cloud scales the Worker Pool by impersonating a service account you create, called the invoker service account. The invoker service account reads and scales the pool through the Cloud Run admin API. It does not run the pool.

caution

This guide uses two service accounts, and they are not interchangeable:

  • The runner service account is the identity the Worker Pool runs as. You set it in Step 2 with gcloud run worker-pools deploy --service-account, and it can be an account you already have. Omit the flag and Cloud Run uses the project's default Compute Engine service account.
  • The invoker service account is the identity Temporal impersonates to read and scale the pool. The Terraform module below creates it, and its email is the invoker_email output you give Temporal in Step 4.

When you create a Worker Deployment in the Temporal Cloud UI (WorkersCreate Worker DeploymentAccess), it provides a Terraform template. Copy it. The template uses the serverless-workers/gcp/cloud-run module, with impersonator_service_account_emails already filled in for your account:

module "serverless-worker-cloud-run" {
source = "github.com/temporalio/terraform-modules//modules/serverless-workers/gcp/cloud-run"

project_id = "<YOUR_GCP_PROJECT>"
invoker_account_id = "temporal-worker-pool-invoker"

impersonator_service_account_emails = [
"<provided by Temporal Cloud>",
]

runner_service_account_email = "temporal-worker-pool-runner@<YOUR_GCP_PROJECT>.iam.gserviceaccount.com"
}

The impersonator_service_account_emails values are specific to your Temporal Cloud account, which is why the snippet above shows a placeholder. Copy them from the template in the UI.

Set these variables:

VariableRequiredDescription
project_idYesThe GCP project that hosts the Worker Pool and the invoker service account.
invoker_account_idYesA name for the invoker service account the module creates. The full email becomes <invoker_account_id>@<project_id>.iam.gserviceaccount.com. The template supplies a name, so change it only if you want a different one.
impersonator_service_account_emailsYesTemporal Cloud's service accounts, granted roles/iam.serviceAccountTokenCreator on the invoker service account so they can impersonate it. Filled in by the template in the UI.
runner_service_account_emailNoThe runner service account from Step 2. The module grants the invoker service account roles/iam.serviceAccountUser on it, which Cloud Run requires to attach that identity when it scales the pool. Leave it unset and the invoker service account gets that grant on the default Compute Engine service account instead.
invoker_display_nameNoDisplay name for the invoker service account. Defaults to Temporal Serverless Worker Pool Invoker.
deploy_rolesNoProject-level Cloud Run roles granted to the invoker service account. Defaults to roles/run.developer. Any role you use instead must include run.workerPools.get and run.workerPools.update.

Make sure you are logged in to the GCP project, then apply the configuration:

terraform init
terraform apply

Terraform prints an invoker_email output. Use it as the --gcp-cloud-run-service-account value when you create the Worker Deployment Version in Step 4.

4. Create Worker Deployment Version

Create a Worker Deployment Version whose compute configuration points at your Worker Pool. The compute configuration tells Temporal where the pool lives and which service account to impersonate to manage it. The deployment name and build ID must match the values in your Worker code.

In the Temporal Cloud UI, go to WorkersCreate Worker Deployment and fill out the required fields:

  • Name — the Worker Deployment name. Must match deployment_name in your Worker code.
  • Build ID — the version identifier. Must match build_id in your Worker code.
  • Compute Provider — select Google Cloud Run.
  • Resource — the Project ID, Region, and Worker Pool from Step 2.
  • Access — the Service Account email Temporal Cloud impersonates: the invoker_email output from Step 3.

Scaling and Lifecycle is optional. Leave the defaults unless you need to change them. If your Worker runs long-running Activities, use Activity Heartbeats so an interrupted Activity resumes from its last recorded progress. See GCP Cloud Run lifecycle.

To verify that Temporal can reach your Worker Pool, go to Workers > Deployments > select your deployment > open the Actions menu on the version and click Validate Connection. This checks that Temporal can impersonate the invoker service account and read the pool. It starts no instance, and it does not exercise the update permission that scaling needs, so a passing validation is not proof that Temporal can scale the pool.

5. Set version as current

Set the version as current. Without this step, Tasks on the Task Queue will not route to the version, and the WCI will not start any instances.

temporal worker deployment set-current-version \
--namespace <YOUR_NAMESPACE> \
--deployment-name my-app \
--build-id build-1

This command asks you to confirm, because it changes which version new Tasks route to. Pass --yes to skip the prompt.

6. Verify deployment

Start a Workflow on the same Task Queue to confirm that the WCI starts a Worker instance and processes the Task.

temporal workflow start \
--namespace <YOUR_NAMESPACE> \
--task-queue my-task-queue \
--type MyWorkflow \
--input '"Hello, serverless!"'

When Tasks arrive with no active pollers, the WCI raises the Worker Pool's instance count. Cloud Run starts an instance, the Worker connects to Temporal, picks up the Task, and processes it.

You can confirm this by checking:

  • Temporal UI: The Workflow execution should show Task completions in the event history.
  • Cloud Run logs: Once the WCI starts an instance, the Worker Pool's logs (gcloud run worker-pools logs read my-temporal-worker-pool-build-1 --region <REGION> --project <YOUR_GCP_PROJECT>) show the Worker startup and Task processing. The pool produces no logs until an instance is running.

If the Workflow does not progress or no instance starts, see Troubleshoot Serverless Workers on GCP Cloud Run.