# gcloud CLI Cheat Sheet — Cloud Deploy, Workflows & IAP

> **Tool:** Google Cloud CLI (gcloud)
> **Category:** Cloud CLIs
> **Verified against:** Google Cloud SDK 553.0.0, flags verified via `gcloud deploy apply --help`, `gcloud
> deploy releases create --help`, `gcloud deploy rollouts approve --help`, `gcloud workflows deploy --help`,
> `gcloud workflows run --help`, `gcloud iap web enable --help`, `gcloud iap web add-iam-policy-binding
> --help`, `gcloud compute start-iap-tunnel --help` run locally, and
> https://docs.cloud.google.com/deploy/docs/config-files for the Cloud Deploy YAML schema, 2026-09-17
> **Official docs:** https://cloud.google.com/sdk/gcloud/reference/deploy, https://cloud.google.com/sdk/gcloud/reference/workflows, and https://cloud.google.com/sdk/gcloud/reference/iap

## 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

```mermaid
flowchart LR
    Release["Release<br/>(one build, one image)"] --> Rollout1["Rollout: dev<br/>(auto-deploys)"]
    Rollout1 --> Rollout2["Rollout: staging<br/>(auto-promotes on success)"]
    Rollout2 --> Gate{"Approval<br/>required?"}
    Gate -->|"yes"| Wait["Waiting for approval"]
    Gate -->|"no"| Rollout3["Rollout: prod"]
    Wait -->|"approved"| Rollout3

    classDef ok fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    classDef warn fill:#fbeee0,stroke:#b8650f,color:#10161c
    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    class Release info
    class Rollout1,Rollout2,Rollout3 ok
    class Gate,Wait warn
```

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

```yaml
# 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
```

```bash
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

```bash
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

```bash
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

```yaml
# 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"
```

```bash
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

```bash
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)

```bash
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:

```yaml
# in the staging Target's strategy block
strategy:
  standard:
    verify: true
    postdeploy:
      actions: ["smoke-test"]
```

```bash
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

```yaml
# .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.
