Cloud Deploy, Workflows & IAP
Verified against Google Cloud SDK 553.0.0, flags verified via `gcloud deploy apply --help`, `gcloud · official docs
What it is and where it fits 🎯#
Three services that share a theme, controlled progression and controlled access, rather than a shared
mechanism. Cloud Deploy is a managed continuous-delivery pipeline for promoting one build artifact
through a sequence of named targets (dev → staging → prod) with approval gates, sitting one layer above the
build-and-push Cloud Build handles on page 09. Cloud Workflows is a managed orchestrator for chaining
API calls and business logic together with retries and branching, without a server of your own running the
orchestration loop. IAP (Identity-Aware Proxy) puts an IAM-authenticated checkpoint in front of a
resource, a compute instance's SSH port or an internal web app, so nothing needs a VPN or a public IP to be
reachable only by authorized people. This page assumes the Compute/GKE/Cloud Run resources from page 02
and the Artifact Registry images from page 09, since a Cloud Deploy pipeline promotes exactly those images.
Core concepts: a Cloud Deploy pipeline's shape#
A delivery pipeline defines the sequence of named targets a release moves through; each target gets its own rollout, and a target can require manual approval before its rollout proceeds, the mechanism behind a real "someone has to click approve before this reaches production" gate, tracked and auditable in the API rather than living as tribal knowledge in a runbook.
Defining a pipeline and its targets#
# deploy-config.yaml
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: my-app-pipeline
description: Promote my-app from dev to production
serialPipeline:
stages:
- targetId: dev
- targetId: staging
- targetId: prod
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: dev
requireApproval: false
run:
location: projects/my-project-id/locations/us-central1
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: prod
requireApproval: true
run:
location: projects/my-project-id/locations/us-central1gcloud deploy apply --file=deploy-config.yaml --region=us-central1
gcloud deploy delivery-pipelines list --region=us-central1
gcloud deploy targets describe prod --region=us-central1 --delivery-pipeline=my-app-pipelineUnlike most gcloud resources on earlier pages, a delivery pipeline and its targets are defined
declaratively in one YAML file and applied together with deploy apply, there's no deploy delivery-pipelines create subcommand at all, apply is the only entry point, consistent with how Cloud
Deploy expects its configuration to be checked into version control rather than built up through individual
imperative commands. requireApproval: true on the prod target is what creates the manual gate in the
core-concepts diagram; run: targets Cloud Run specifically, a gke: block (shown in Cloud Deploy's own
docs) targets a GKE cluster instead.
Creating a release and promoting it#
gcloud deploy releases create release-$(git rev-parse --short HEAD) \
--delivery-pipeline=my-app-pipeline --region=us-central1 \
--images=my-app=us-central1-docker.pkg.dev/my-project-id/my-app-images/api:$(git rev-parse --short HEAD)
gcloud deploy rollouts list --release=release-a1b2c3d --delivery-pipeline=my-app-pipeline --region=us-central1
gcloud deploy rollouts approve staging-to-prod-rollout \
--release=release-a1b2c3d --delivery-pipeline=my-app-pipeline --region=us-central1releases create is the single command that kicks off promotion through the entire pipeline, it
automatically deploys to dev (the first stage), and each subsequent stage either auto-proceeds or waits
for rollouts approve, depending on that target's requireApproval setting. --images maps a symbolic
name (referenced inside a Kubernetes manifest or Cloud Run service spec as a placeholder) to the actual
image URI, the substitution Cloud Deploy performs at render time for each target.
Rolling back#
gcloud deploy releases list --delivery-pipeline=my-app-pipeline --region=us-central1
gcloud deploy rollouts create rollback-rollout \
--release=release-<previous-known-good-sha> \
--delivery-pipeline=my-app-pipeline --region=us-central1 --to-target=prodA rollback is just a new rollout targeting a previous, already-validated release, not a special destructive
operation, since every release Cloud Deploy has ever created stays addressable by name. This is the same
"redeploy an old known-good artifact" pattern as reverting a Cloud Run URL map's default service on page
07, applied at the pipeline level instead of the load-balancer level.
Cloud Workflows: defining and deploying a workflow#
# order-processing.yaml
main:
params: [input]
steps:
- validate_order:
call: http.post
args:
url: https://validation-api.example.com/check
body: ${input}
result: validation_result
- check_valid:
switch:
- condition: ${validation_result.body.valid == true}
next: process_payment
next: reject_order
- process_payment:
call: http.post
args:
url: https://payments-api.example.com/charge
body: ${input}
result: payment_result
next: end
- reject_order:
return: "Order validation failed"gcloud workflows deploy order-processing --source=order-processing.yaml \
--location=us-central1 --service-account=workflow-runner@my-project-id.iam.gserviceaccount.com
gcloud workflows run order-processing --location=us-central1 \
--data='{"orderId": "12345", "amount": 49.99}'
gcloud workflows executions list --workflow=order-processing --location=us-central1Each step's call typically invokes an HTTP endpoint, another GCP API, or a Cloud Function/Cloud Run
service, with the workflow engine itself handling retries, timeouts, and the branching (switch) logic
between steps, no server of your own has to stay running to hold that orchestration state between calls.
workflows run blocks and waits for completion, appropriate for testing; a real production trigger is
usually Cloud Scheduler or Eventarc (page 05) invoking the workflow asynchronously instead.
IAP: gating a Compute VM's SSH access with no public IP or VPN#
gcloud compute start-iap-tunnel my-instance 22 \
--local-host-port=localhost:2222 --zone=us-central1-a
ssh -p 2222 my-user@localhost # in a separate terminal, once the tunnel above is runningThis is the mechanism behind gcloud compute ssh --tunnel-through-iap shown on page 02, made explicit:
start-iap-tunnel opens a local port that forwards, encrypted and IAM-authenticated, to the instance's
actual port, entirely over Google's own network, no SSH-facing firewall rule and no VPN client needed. The
instance can have --enable-private-nodes-style no-public-IP configuration (page 02/03) and still be
reachable this way, since the tunnel originates from IAP's own infrastructure, not from the caller's IP
directly.
IAP: gating a web application (backend service or App Engine)#
gcloud iap web enable --resource-type=backend-services --service=my-internal-app-backend
gcloud iap web add-iam-policy-binding \
--resource-type=backend-services --service=my-internal-app-backend \
--member="group:internal-tools-users@example.com" --role="roles/iap.httpsResourceAccessor"Enabling IAP on a backend service (the same backend service resource from page 07) puts an
authentication checkpoint in front of it, only identities explicitly granted roles/iap.httpsResourceAccessor
can reach it at all, everyone else gets redirected to a Google sign-in prompt and then denied. This is the
standard pattern for an internal admin tool or dashboard that needs to be reachable over the internet (so a
remote team can use it) without being genuinely public.
Note
The OAuth consent-screen bootstrap commands (gcloud iap oauth-brands create) are deprecated and no
longer functional as of March 2026, Google shut down the IAP OAuth Admin APIs those commands depended
on. A project's OAuth consent screen (required once, before iap web enable works) is now configured
through the Console's "OAuth consent screen" page instead; iap web enable/add-iam-policy-binding
themselves are unaffected and still the correct gcloud path for everything after that one-time setup.
Real-world scenario: a staged rollout with an automated canary gate#
A payments team wants staging to auto-promote only if a post-deploy smoke test actually passes, not just
because the deploy itself succeeded:
# in the staging Target's strategy block
strategy:
standard:
verify: true
postdeploy:
actions: ["smoke-test"]gcloud deploy apply --file=deploy-config.yaml --region=us-central1
gcloud deploy rollouts describe <rollout-id> \
--release=<release-name> --delivery-pipeline=my-app-pipeline --region=us-central1 \
--format="value(phases[].deploymentJobs.postdeployJob.state)"verify: true plus a postdeploy action is what turns "the deploy API call succeeded" into "the deployed
version was actually verified working" before Cloud Deploy considers that stage's rollout complete, the
distinction matters because a deploy that succeeds at the infrastructure level can still ship a broken
application.
Real-world scenario: replacing a bastion host with IAP#
A team maintaining a traditional bastion host (a single VM with a public IP, everyone SSHes through it to reach private instances) wants to remove that single point of failure and audit gap:
- Confirm every private instance's firewall allows SSH only from IAP's known source range
(
35.235.240.0/20), not from the bastion's IP or0.0.0.0/0 - Grant
roles/iap.tunnelResourceAccessorper engineer (or per group) on the specific instances they need, not a blanket project-wide grant - Confirm
gcloud compute start-iap-tunnelreaches each instance successfully before decommissioning the bastion, not after - Decommission the bastion host and its public IP once every engineer has validated IAP tunnel access works for their actual workflow
Tip
Every IAP tunnel connection is a real, individually-authenticated IAM event, unlike a shared bastion host where "who actually SSHed through it and when" often depends on the bastion's own local audit logging being configured correctly. IAP access shows up in Cloud Logging's Data Access logs by default, a genuine audit-trail improvement, not just a convenience one.
CI/CD integration recipe: releasing through Cloud Deploy from GitHub Actions#
# .github/workflows/release.yml
name: Release
on:
push:
tags: ["v*"]
permissions:
id-token: write
contents: read
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
service_account: deploy-releaser@my-project-id.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- run: |
gcloud deploy releases create release-${{ github.ref_name }} \
--delivery-pipeline=my-app-pipeline --region=us-central1 \
--images=my-app=us-central1-docker.pkg.dev/my-project-id/my-app-images/api:${{ github.sha }}Triggering on a version tag rather than every push to main keeps "build and push a new image" (page 09,
which should still run on every merge) separate from "actually start promoting a release through
production," a deliberate decision point rather than an automatic consequence of every commit landing.
Common pitfalls#
- Trying
gcloud deploy delivery-pipelines createand finding it doesn't exist. Pipelines and targets are defined in YAML and applied together withdeploy apply, there's no separate imperative create command for either. - Setting
requireApproval: falseon a production target "temporarily" and forgetting to revert it. The approval gate is a config value like any other, it doesn't re-enable itself. - Attempting
gcloud iap oauth-brands createand hitting a dead API. See the NOTE above, that bootstrap step moved to the Console; only the OAuth brand creation is affected, not ongoingiap webusage. - Granting
roles/iap.tunnelResourceAccessorproject-wide instead of per instance. Grants tunnel access to every instance in the project, not just the ones a given engineer actually needs. - Assuming a Cloud Workflows step retries automatically on failure with no configuration. Retry
behavior is explicit per step (a
retryblock), an unconfigured step that fails simply fails the execution.
Exit codes#
0 success, non-zero on any API/validation error, gcloud deploy releases create returns as soon as the
release object is created and the first stage's rollout starts, not once the whole pipeline finishes
promoting, deploy rollouts list/describe is how to check a specific stage's actual completion state
rather than trusting the triggering command's exit code for anything beyond "the release was accepted."
When to reach for something else#
For a simple single-environment deploy with no approval gates or staged promotion, gcloud run deploy
directly (page 02) is meaningfully less overhead than standing up a full Cloud Deploy pipeline, reach for
Cloud Deploy specifically when multiple environments and a real promotion/approval process are genuinely
needed. For orchestration logic that's already comfortably expressed as application code with normal
retries and error handling, a Cloud Workflows definition adds indirection without benefit, it earns its
place when the orchestration itself needs to survive independently of any one running process (a
long-running, multi-day approval chain, say). For declarative, reviewable pipeline and workflow
provisioning across environments, keeping the YAML files this page already uses under version control (as
deploy apply/workflows deploy both expect) already gets most of the reviewability Terraform would add
elsewhere on this site; Terraform's google_clouddeploy_delivery_pipeline/google_workflows_workflow
resources remain a valid alternative if a team prefers one unified provisioning tool for everything.