Verified11 commandsAI-assisted

Cloud Deploy, Workflows & IAP

.md

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#

Diagram

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-central1
gcloud 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-pipeline

Unlike 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-central1

releases 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=prod

A 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-central1

Each 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 running

This 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 or 0.0.0.0/0
  • Grant roles/iap.tunnelResourceAccessor per engineer (or per group) on the specific instances they need, not a blanket project-wide grant
  • Confirm gcloud compute start-iap-tunnel reaches 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 create and finding it doesn't exist. Pipelines and targets are defined in YAML and applied together with deploy apply, there's no separate imperative create command for either.
  • Setting requireApproval: false on 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 create and hitting a dead API. See the NOTE above, that bootstrap step moved to the Console; only the OAuth brand creation is affected, not ongoing iap web usage.
  • Granting roles/iap.tunnelResourceAccessor project-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 retry block), 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.