Skip to main content

Google Cloud: Deploy an Authenticated Cloud Run Service

Google Cloud provides computing, storage, databases, and networking on demand. You can rent a virtual machine or submit an application and let the platform start instances and handle requests. You do not need to memorize its product catalog to begin. First understand where resources belong, who can operate them, and who pays.

This guide follows a small Python HTTP app that returns JSON: run it locally, deploy it to Cloud Run with authenticated access, then update, roll back, and clean up. It needs no database, Kubernetes cluster, or service account key.

The local steps do not call Google Cloud, although installing tools and downloading Python, dependencies, or container images can require network access. Cloud commands are instructions for readers who choose to proceed: they sign in, upload source, change resources, and may incur charges. Stop after the local check if you only want to understand the workflow.

1. Understand projects, locations, and permissions

Which project owns the resource?

Google Cloud's administrative hierarchy consists of an organization, optional folders, projects, and their resources. An organization usually represents a company or institution; folders group resources and delegate administration. Ancestor IAM grants and organization policies affect descendants, so a project is not automatically a sandbox isolated from company policy. A personal exercise does not require creating an organization or folder. See the resource hierarchy documentation.

A project groups resources, settings, permissions, and metadata. Keep these identifiers separate:

NamePurposeUse in this guide
Project display nameA human-readable labelDo not substitute it for the project ID in commands
Project IDA stable identifier established when the project is createdAssign it to PROJECT_ID
Project numberA numeric identifier assigned by GoogleUsed in some default service account and service-agent names
Billing accountPays for and accounts for usage; several projects can share oneConfirm its association and who is responsible for payment

A billing account determines who pays, not who can call an API. An API is a programmatic interface to a cloud service. Enabling one in a project makes the service available; it does not grant arbitrary callers permission to use it.

Where does the resource run?

A region is a geographic deployment location; a zone is a subdivision within a region. A Compute Engine virtual machine generally requires a zone, whereas this Cloud Run service uses a region. Resource scope depends on the product rather than one universal rule. See the Google Cloud overview.

Choose REGION based on users, data residency, dependent services, product availability, and regional pricing. A database in another region can add latency and transfer charges. Do not assume every product is available in every region.

Who is acting?

Identity and Access Management (IAM) answers “who can do what to which resource?” A principal can be a person, group, service account, or federated identity. A role groups permissions, and a grant applies at a scope such as a project or an individual service. Project-level grants generally affect more resources than service-level grants.

A service account is an application identity, not a server or a JSON file you must download. A person deploys, a build system creates an image, and an application runs: those jobs need different permissions. Prefer narrowly scoped predefined roles instead of treating Owner or Editor as a troubleshooting tool. See Cloud Run service identity.

2. Choose compute and data products

Choose compute according to the operational work you need to control. These are starting points, not a complete feature comparison; the Google Cloud overview describes the product categories.

RequirementStarting pointWork that remains yours
Control an OS, install persistent daemons, or migrate a conventional serverCompute EngineVM lifecycle, OS updates, networking, and availability design
Run a stateless HTTP container without managing a clusterCloud RunApplication code, the container contract, permissions, and scaling settings
Use an existing Kubernetes platform or require its orchestration APIsGoogle Kubernetes Engine (GKE)Kubernetes workload and cluster decisions; not a prerequisite for a first web service

A container image packages an application and its runtime dependencies. An instance is a running environment started from that image. “Stateless” means a request does not depend on files or memory left on a particular instance. This app returns fixed content, which suits Cloud Run.

Choose data products by their data model rather than treating everything called storage as interchangeable:

Data requirementStarting pointImportant distinction
Images, uploads, backups, and other objectsCloud StorageObject storage, not the container's local disk or a relational database
Business data with related tables, transactions, and SQL queriesCloud SQLConnections, migrations, database authentication, and capacity still need design
Document-oriented application dataFirestoreDesign around its document and query model rather than copying relational joins
SQL analysis of large historical datasetsBigQueryAn analytical system, not this tiny app's default transactional database

This tutorial adds no database. Establish deployment and identity first, then choose a product for an actual data requirement.

3. Separate CLI login, application credentials, and runtime identity

A command-line interface (CLI) lets you operate software by typing commands; gcloud is Google Cloud's CLI. Authentication establishes who you are; IAM authorization determines what you may do. Signing in successfully does not grant deployment permission. Google's Application Default Credentials (ADC) documentation distinguishes CLI credentials from application credentials.

SituationMechanismNeeded here?
A person operates cloud resources with gcloudgcloud auth loginFor the cloud steps
Local code calls APIs through Google client librariesgcloud auth application-default login configures local ADCNo; this app calls no Google APIs
An application on Cloud Run calls Google APIsAn attached runtime service account with platform-provided credentials available through ADCAn identity is attached, but this example receives no additional data-access roles
External continuous integration or code on another cloud accesses Google CloudWorkload Identity FederationExplained here, not configured

ADC is a convention for finding credentials: it checks the relevant environment configuration, local ADC credentials, and attached service account credentials in that order. A successful local ADC login does not switch the CLI's active account; gcloud auth login does not automatically configure credentials for every local application.

Workload Identity Federation lets an external program exchange an identity from a trusted provider for short-lived credentials, then access authorized resources directly or impersonate an authorized service account. It reduces the maintenance and exposure of long-lived keys. Workforce Identity Federation, intended for people, is a different concept.

Do not put credential JSON, tokens, or service account private keys in source, images, or logs. This exercise requires no service account key creation or download.

4. Create and check the app locally

Prepare tools and a dedicated directory

Use an installed Python 3.12, uv, and curl. Commands use Bash/zsh syntax; on Windows, use an appropriate environment such as WSL rather than pasting them unchanged into PowerShell. For environments, locks, and execution, see Python environments with uv.

Start in a new empty directory so a later deployment does not upload an existing private project:

mkdir cloud-run-hello
cd cloud-run-hello
uv init --bare --python 3.12
uv add 'flask~=3.0' 'gunicorn~=23.0'

Flask handles HTTP routes. Gunicorn is the server that accepts requests and runs the Python application. The dependency constraints follow Google's Python quickstart, not a claim about the latest versions. Keep pyproject.toml and uv.lock; uv manages .venv, and the lockfile records the resolved dependency versions.

Create main.py:

from flask import Flask

app = Flask(__name__)


@app.get("/")
def hello():
return {"message": "Hello from Cloud Run", "version": "v1"}, 200

Run this in the first terminal:

uv run gunicorn --bind 127.0.0.1:8080 main:app

main:app means the object named app in main.py. In a second terminal, run:

curl --fail --silent --show-error http://127.0.0.1:8080/

Expect HTTP 200 and JSON containing message and "version":"v1"; spacing and key order do not matter. If the connection is refused, check that the first terminal is still running the server on the correct port. This request stays on your machine and needs no cloud account. Stop the server with Ctrl+C afterward: the later proxy also uses port 8080.

Make the container build explicit

Buildpacks detect an application's language and build a container image from its source. Here, create a Dockerfile so Cloud Build explicitly uses uv to install locked dependencies instead of relying on buildpacks to recognize a uv project:

FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.12.13 /uv /uvx /bin/
WORKDIR /app
ENV UV_NO_DEV=1
COPY pyproject.toml uv.lock main.py ./
RUN uv sync --locked
ENV PATH="/app/.venv/bin:$PATH"
CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:${PORT:-8080} main:app"]

These image tags come from the uv Docker integration examples; they are not a latest-version promise. Tags can be reassigned. For stronger production reproducibility, verify and pin image digests. uv sync --locked checks that the lockfile agrees with the project rather than silently rewriting it during a build. This simple project declares no packaging backend.

The Cloud Run container contract requires the ingress process to listen on 0.0.0.0 and the platform-provided PORT. Do not carry the local test's 127.0.0.1 binding into the cloud container. TLS, the protocol that encrypts HTTPS connections, terminates outside the container, so the app needs no embedded certificate. exec passes termination signals to Gunicorn.

The writable container filesystem consumes instance memory and is not persistent. Do not rely on files surviving instance termination; put uploads in suitable persistent storage. Keeping minimum instances does not turn a local directory into a durable disk. For startup failures, first check that Gunicorn is installed and binds to the right address and port, then inspect startup logs.

Create both .dockerignore and .gcloudignore, with the following contents in each:

.git/
.venv/
__pycache__/
*.pyc
.env
.env.*
*.pem
*.key
*credentials*.json
*service-account*.json

.gcloudignore controls source upload; .dockerignore controls the Docker build context and cannot independently prevent a source upload. The deployment reference explains source directories and ignore files. Keep only tutorial files in this directory and review the file list before uploading. These patterns do not prove an arbitrary directory contains no secrets. Do not exclude Dockerfile, main.py, pyproject.toml, or uv.lock.

5. Decide whether to proceed with billable cloud steps

You need an existing dedicated tutorial project, an associated billing account you are authorized to use, a suitable region, and an administrator who can enable APIs and grant access. An organization may restrict regions, build sources, image repositories, and service accounts. Confirm its policies rather than working around them.

Set up cost notifications first

In Cloud Billing, create a budget scoped to the tutorial project, choose notification thresholds, and check the recipients. An alerts-only budget sends notifications; it does not automatically stop services at that amount.

As of 2026-09-12, Google also documents optional spend-cap budgets, labeled preview in the budget entry page. Each monthly budget covers one project and one eligible service, with Cloud Run among the eligible services. This is not an exact ceiling for your entire billing account: new usage can be blocked while in-flight requests, reporting delays, and retained resources still produce payable charges. Do not assume a Cloud Run cap also covers Cloud Build, image storage, or databases.

Cloud Run pricing distinguishes regions, billing modes, and additional charges. Builds, Artifact Registry images, supporting storage, outbound traffic, and extra networking resources can be billed separately. Free usage is aggregated by billing account, not renewed for every project. This tutorial neither promises a free exercise nor depends on trial credit.

The later --min=0 setting allows scale-to-zero but does not guarantee immediate removal of idle instances or no charges. --max=2 limits capacity, not currency. Quotas limit particular resources or usage and are not a replacement for billing controls.

Install the CLI and select its context

Follow the platform-specific official Google Cloud CLI installation instructions. The CLI's Python environment is separate from the app's uv environment.

Replace every placeholder before performing these cloud steps:

export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="YOUR_SUPPORTED_REGION"
export SERVICE="tutorial-hello"
export RUNTIME_SA_NAME="tutorial-hello-runtime"
export RUNTIME_SA_EMAIL="${RUNTIME_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud auth login
gcloud config set project "$PROJECT_ID"

gcloud init is an alternative guided login and setup; there is no need to repeat both workflows. CLI configurations hold defaults, not security boundaries. Resource commands below still specify the project and region explicitly. With multiple accounts, confirm the active identity in the CLI or console before acting. You do not need gcloud auth application-default login for this app.

6. Have an administrator prepare identities and build resources

Source deployment uses Cloud Build to build an image, stores the image in Artifact Registry, then creates a Cloud Run revision. A revision is an immutable deployment configuration for a service. Deployer, build, and runtime permissions serve different purposes. See the source deployment role requirements.

IdentityPermission needed hereScope
Human deployerroles/run.sourceDeveloper, roles/serviceusage.serviceUsageConsumerTutorial project
Human deployerroles/iam.serviceAccountUser, allowing the service to run as the chosen identityRuntime service account only
Actual build accountroles/run.builderTutorial project
Runtime service accountNo additional application data-access rolesThe example calls no Google APIs
Invokerroles/run.invokerThis Cloud Run service only
Bootstrap administratorPermissions to enable APIs, create accounts, configure IAM, and administer service access policyAs authorized by the organization; not the ordinary deployer role

Google-managed service agents perform platform operations. They are neither the build account nor the app identity; do not change their grants arbitrarily to fix errors. Viewing logs can require additional log-reading permission. Minimum deployment roles do not imply access to every operational task.

An authorized administrator performs the following setup. Do not recreate existing resources with the same names, or assume a tutorial-style name proves exclusive ownership:

gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
artifactregistry.googleapis.com --project "$PROJECT_ID"

gcloud iam service-accounts create "$RUNTIME_SA_NAME" \
--display-name="Tutorial hello runtime" --project "$PROJECT_ID"

Confirm the actual default build account in the project's Cloud Build settings before assigning BUILD_SA_EMAIL. The current source-deployment documentation describes the Compute Engine default service account as the default, but historical configuration and organization policy can differ. Do not guess an email from project age. This example uses the project's effective default build identity; the shell variable only grants it access below and does not select a different build identity.

export DEPLOYER_MEMBER="user:YOUR_EMAIL"
export BUILD_SA_EMAIL="YOUR_CONFIRMED_BUILD_SERVICE_ACCOUNT_EMAIL"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="$DEPLOYER_MEMBER" --role=roles/run.sourceDeveloper

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="$DEPLOYER_MEMBER" --role=roles/serviceusage.serviceUsageConsumer

gcloud iam service-accounts add-iam-policy-binding "$RUNTIME_SA_EMAIL" \
--project "$PROJECT_ID" --member="$DEPLOYER_MEMBER" \
--role=roles/iam.serviceAccountUser

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${BUILD_SA_EMAIL}" --role=roles/run.builder

Record which grants are new to this exercise. IAM changes can take time to propagate; confirm the principal and scope before retrying rather than granting Editor. Service Account User on the runtime identity lets the deployer use that identity; it does not give the application project-administration rights. See service identity configuration.

Source deployment can create or reuse an Artifact Registry repository and retain supporting resources such as build source. Have the administrator confirm that repository creation, build, and storage policies permit this workflow. Record the actual repository, images, and source locations used; do not assume an automatically created repository belongs exclusively to this example.

7. Deploy and invoke with authentication

From cloud-run-hello, the authorized deployer runs:

gcloud run deploy "$SERVICE" \
--source . \
--project "$PROJECT_ID" \
--region "$REGION" \
--service-account "$RUNTIME_SA_EMAIL" \
--port 8080 \
--min=0 \
--max=2 \
--no-allow-unauthenticated \
--invoker-iam-check

This uploads source and performs a potentially billable cloud build. With a Dockerfile present, --source uses it; otherwise it uses Google buildpacks. --min and --max are service-level settings, unlike the revision-level --min-instances and --max-instances. See the deployment flag reference.

The access flags disallow anonymous authorization and enable invoker IAM checks. “Private” here means IAM-authenticated access, not a private network address or restricted network ingress. Explicit access-policy changes can require service IAM administration beyond the minimum source-deployer roles. Have an administrator authorize or perform the relevant operation rather than enabling anonymous access to bypass a permission error.

If the build fails, inspect this Cloud Build run's logs and failing step. Check the lockfile, Dockerfile, APIs, effective build identity, and repository permissions. No application entries in Cloud Run runtime logs is normal when the build has not succeeded. Organization-policy denials also need administrator attention.

Once the service exists, an administrator with service IAM policy permission grants the designated invoker access:

gcloud run services add-iam-policy-binding "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION" \
--member="$DEPLOYER_MEMBER" --role=roles/run.invoker

Using the deployer identity, start the local Cloud Run proxy:

gcloud run services proxy "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION"

Leave it running and request the service from another terminal:

curl --fail --silent --show-error http://127.0.0.1:8080/

Expect the v1 JSON again. Although the address is localhost, this request travels through the proxy to Google Cloud with the active account's identity and can incur charges. Make sure the earlier local Gunicorn process has stopped. An occupied port must not lead you to mistake a local response for successful cloud invocation.

For a 403, check the active account and its roles/run.invoker grant on this service. An actAs error during deployment instead points to permission on the runtime identity. Do not add allUsers as a debugging shortcut. Production service-to-service calls should use ID tokens intended for the target service, not a proxy on a developer's computer or copied tokens.

8. Inspect logs, update, and roll back

Read service logs and list revisions:

gcloud run services logs read "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION" --limit=20

gcloud run revisions list --service "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION"

Use logs to distinguish startup problems, application exceptions, and request failures. Do not log credentials or personal request data. If viewing is denied, request appropriate read-only access rather than broadening the runtime account's permissions.

A service owns its URL and traffic settings; a revision holds a deployment's configuration. In the console or list, identify and record the v1 revision currently receiving traffic. Do not assume it is the first row:

export PREVIOUS_REVISION="YOUR_VERIFIED_V1_REVISION"

Change v1 to v2 in main.py. Stop the proxy and repeat section 4's uv run and local curl check. Confirm v2, then stop the local server. Repeat the full deployment command from section 7.

In the console, confirm the new revision is ready. When you intend to send all traffic to it, explicitly select the latest ready revision:

gcloud run services update-traffic "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION" \
--to-latest

Do not run concurrent deployments during this exercise. --to-latest targets the latest ready revision and also affects how later deployments receive traffic. Restart the proxy and confirm v2. Existing traffic assignments can affect new deployments, so “deployment succeeded” alone does not establish which version serves requests. See the traffic update reference.

To return to the verified v1 revision:

gcloud run services update-traffic "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION" \
--to-revisions="${PREVIOUS_REVISION}=100"

Invoke through the proxy again and expect v1. This rolls back traffic; it does not reverse a database migration, delete new images, or undo writes to external systems. Check the intended traffic target explicitly when you deploy again later.

9. Add networking when a private database requires it

A Virtual Private Cloud (VPC) provides virtual networking, routing, and firewall controls. In Google Cloud, VPC networks are global and subnets are regional. Network connectivity and IAM are separate checks: reaching a database does not authorize reading it, and having permission does not guarantee a network path.

Cloud Run ingress authentication answers “who can request this service?” Connecting to a private database asks “how can this service send traffic to the database?” Direct VPC egress lets Cloud Run send traffic to a VPC without a Serverless VPC Access connector. Do not create a connector merely to return a JSON response.

Keep these terms distinct:

  • Private Services Access uses an allocated address range and private connection to reach supported resources in a service producer's network. That connection uses VPC Network Peering.
  • Private Service Connect provides a different private-service access mechanism, such as service endpoints in a consumer VPC.
  • Cloud Run VPC egress configures the path from running instances to a VPC. It is not itself either managed-service connection mechanism and does not grant database login permission.

For a real Cloud SQL integration, design the connection for the selected instance and networking method, then configure application identity and database authentication. Cross-project API calls do not universally require VPC peering, and peering does not grant API permissions. This example remains database-free with no additional VPC resources.

10. Clean up only tutorial resources

Stop the local proxy, verify the project ID, region, and service name, and confirm in the console that the target belongs to this exercise. The next commands delete resources. Keep confirmation prompts enabled; do not add automatic confirmation flags.

Delete the Cloud Run service:

gcloud run services delete "$SERVICE" \
--project "$PROJECT_ID" --region "$REGION"

Only after confirming that no other service uses the runtime account, delete that service account:

gcloud iam service-accounts delete "$RUNTIME_SA_EMAIL" \
--project "$PROJECT_ID"

Deleting the service does not automatically remove all built images, uploaded source, logs, or other storage. Use the locations recorded earlier to inspect and remove only unused tutorial images or source objects in the console. Do not delete an entire shared automatic repository. Ask an administrator when retention policies or ownership are unclear.

An administrator can remove IAM bindings added for this exercise when they are no longer needed, but a role name alone does not prove a binding was newly added. Do not remove preexisting grants, delete the default build account, disable shared APIs, or delete the entire project instead of checking individual resources.

Finally, inspect billing details and remaining resources. Charges can appear late. If costs continue, examine builds, images, storage, logs, networking, and other dependencies separately; zero service requests does not prove all charges have stopped. For quota or rate errors, inspect the project, region, and quota named in the error rather than retrying indefinitely or confusing quotas with free allowances.

For other practical tools, return to Tools & Workflows.

Explore connectionsOpen network