Using Google Cloud Build with Pulumi
Google Cloud Build is Google Cloud’s serverless CI/CD platform. It runs builds as a series of container-image steps defined in a cloudbuild.yaml file, started by triggers that respond to repository events such as pull requests, branch pushes, and tags.
You run Pulumi in a build step by using one of Pulumi’s official Docker images as the step’s container image. Each image bundles the Pulumi CLI with a language runtime — pulumi/pulumi-nodejs, pulumi/pulumi-python, pulumi/pulumi-go, pulumi/pulumi-dotnet, and pulumi/pulumi-java — so a step can install dependencies and run Pulumi commands against a program written in any supported language and targeting any cloud provider.
This guide assumes Pulumi Cloud as your backend. Pulumi Cloud isn't required to run Pulumi in CI/CD — Pulumi also supports self-managed backends — but the access token, OIDC, and ESC features described here are specific to Pulumi Cloud.
Prerequisites
Before you begin, make sure you have:
- A Pulumi Cloud account and organization.
- A Google Cloud project with the Cloud Build API enabled.
- A source repository that Cloud Build can build from. Cloud Build connects to repositories hosted on GitHub, GitLab, or Bitbucket through a repository connection. (Cloud Source Repositories, Google’s own hosted Git service, has been closed to new customers since June 2024.)
- A Pulumi program committed to that repository. If you don’t have one yet, follow a Get started guide.
Authenticate with Pulumi Cloud
Give your pipeline a Pulumi Cloud identity in one of two ways. Choose one — you don’t need both:
- A stored access token — create a Pulumi access token and keep it in Secret Manager, where a build step reads it at runtime.
- OIDC — exchange a short-lived OpenID Connect token for a Pulumi access token at build time, so no long-lived credential is stored anywhere. Prefer this where your CI/CD system supports it well.
This guide’s examples use the stored-token path, because Cloud Build has no Pulumi-maintained OIDC integration yet — see Authenticate without a stored token using OIDC below.
Pulumi ESC (Environments, Secrets, and Configuration) then supplies cloud credentials, secrets, and configuration to your Pulumi program. Because ESC delivers those values the same way whether the consumer is a build step or a developer’s machine, a single environment definition works in both places — you don’t store separate cloud provider keys in Secret Manager.
Store the access token in Secret Manager
Create a Pulumi access token, preferring an organization or team token over a personal token so the pipeline’s identity isn’t tied to an individual.
Store the token in Secret Manager and grant the Cloud Build service account permission to read it:
# Store the token as a secret.
printf '%s' "$PULUMI_ACCESS_TOKEN" | gcloud secrets create pulumi-access-token --data-file=-
# Grant the Cloud Build service account read access to the secret.
gcloud secrets add-iam-policy-binding pulumi-access-token \
--member="serviceAccount:CLOUD_BUILD_SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
A build configuration then exposes the secret to a step as the PULUMI_ACCESS_TOKEN environment variable through its availableSecrets block, as shown in the examples below.
Authenticate without a stored token using OIDC
You can avoid storing a static token by having Cloud Build obtain a short-lived OpenID Connect (OIDC) token at build time. A build step can request an OIDC id_token for the build’s Google Cloud service account, and Pulumi Cloud can register that as a trusted OIDC issuer and exchange it for a short-lived Pulumi access token.
Unlike GitHub Actions and GitLab CI, Cloud Build has no Pulumi-maintained action or component that performs this exchange for you — you would script the token request and the pulumi login exchange yourself. Until a dedicated integration exists, the stored-token path above is the simpler and recommended choice, and it’s what the rest of this guide uses.
The trunk-based development workflow
The most common way to run Pulumi in CI/CD follows a trunk-based development model: work merges into a single main branch, and deployments flow outward from there. This guide splits that across two build configurations:
cloudbuild-preview.yamlrunspulumi previewon every pull request, surfacing the proposed changes for review.cloudbuild-deploy.yamlrunspulumi upwhen changes land — to staging on a push tomain, and to production on arelease-*tag.
Both configurations install your program’s dependencies and run Pulumi from one of the official pulumi/pulumi-* images. The examples assume a Pulumi program in an infra/ directory and stacks named acme/website/staging and acme/website/production. Only the step image and the dependency-install command differ between languages.
# cloudbuild-preview.yaml
steps:
- name: 'pulumi/pulumi-nodejs'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
npm install
pulumi preview --stack acme/website/staging
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-deploy.yaml
steps:
- name: 'pulumi/pulumi-nodejs'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
npm install
if [ -n "$TAG_NAME" ]; then
pulumi up --yes --stack acme/website/production
else
pulumi up --yes --stack acme/website/staging
fi
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-preview.yaml
steps:
- name: 'pulumi/pulumi-python'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
pip install -r requirements.txt
pulumi preview --stack acme/website/staging
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-deploy.yaml
steps:
- name: 'pulumi/pulumi-python'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
pip install -r requirements.txt
if [ -n "$TAG_NAME" ]; then
pulumi up --yes --stack acme/website/production
else
pulumi up --yes --stack acme/website/staging
fi
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-preview.yaml
steps:
- name: 'pulumi/pulumi-go'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
go mod download
pulumi preview --stack acme/website/staging
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-deploy.yaml
steps:
- name: 'pulumi/pulumi-go'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
go mod download
if [ -n "$TAG_NAME" ]; then
pulumi up --yes --stack acme/website/production
else
pulumi up --yes --stack acme/website/staging
fi
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-preview.yaml
steps:
- name: 'pulumi/pulumi-dotnet'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- 'pulumi preview --stack acme/website/staging'
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-deploy.yaml
steps:
- name: 'pulumi/pulumi-dotnet'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
if [ -n "$TAG_NAME" ]; then
pulumi up --yes --stack acme/website/production
else
pulumi up --yes --stack acme/website/staging
fi
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-preview.yaml
steps:
- name: 'pulumi/pulumi-java'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- 'pulumi preview --stack acme/website/staging'
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
# cloudbuild-deploy.yaml
steps:
- name: 'pulumi/pulumi-java'
dir: 'infra'
entrypoint: 'bash'
args:
- '-c'
- |
if [ -n "$TAG_NAME" ]; then
pulumi up --yes --stack acme/website/production
else
pulumi up --yes --stack acme/website/staging
fi
secretEnv: ['PULUMI_ACCESS_TOKEN']
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/pulumi-access-token/versions/latest
env: 'PULUMI_ACCESS_TOKEN'
options:
logging: CLOUD_LOGGING_ONLY
Pulumi runs non-interactively inside Cloud Build, so pulumi up --yes applies changes without a confirmation prompt. For C# and Java, the language runtime resolves and builds dependencies as part of the Pulumi run, so no separate install step is needed.
The deploy configuration reads the built-in $TAG_NAME substitution — which Cloud Build sets only for tag-triggered builds — to decide whether to update the staging or the production stack. The availableSecrets block reads the access token from Secret Manager and exposes it to the step as PULUMI_ACCESS_TOKEN, and logging: CLOUD_LOGGING_ONLY lets the build run without a Cloud Storage logs bucket.
To promote a release, push a tag that matches the release-* pattern:
git tag release-2026-05-21
git push origin release-2026-05-21
Keeping production on its own stack and deploying it only from a tag makes each production update a single, traceable Git operation, and ensures production never deploys from an untested commit.
To let reviewers exercise a change in a live environment, pair the preview build with a Review Stack, which provisions an ephemeral stack for the pull request and destroys it when the pull request closes.
PULUMI_CI_SYSTEM environment variable, along with the PULUMI_CI_* fallback variables, in your build steps. See adding support for CI/CD systems.Report results on pull requests
By default, the output of a pulumi preview build lands in the Cloud Build logs. To surface the proposed infrastructure changes on the pull request itself, connect your repository to Pulumi Cloud with a version control integration.
These integrations work independently of Cloud Build: Pulumi Cloud posts a summary of resource changes as a pull request comment and status check, and links each stack update back to the commit and pull request that produced it. Pulumi maintains integrations for popular version control systems — see the version control integrations documentation for the current list and setup instructions.
Connect a repository and create a trigger
A trigger ties a repository event to a build configuration. To run the two build configurations above, create three triggers on the same repository:
- Connect your GitHub, GitLab, or Bitbucket repository to Cloud Build by creating a repository connection in the region where you intend to run builds.
- Create a pull request trigger that runs
cloudbuild-preview.yamlwhen a pull request targetsmain. - Create a push trigger that runs
cloudbuild-deploy.yamlon pushes to themainbranch. - Create a second push trigger that runs
cloudbuild-deploy.yamlon tags matchingrelease-*.
Because the deploy configuration inspects $TAG_NAME to choose the target stack, both push triggers can share the one file.
Manage triggers as code
You can define those triggers — and the repository connection itself — as part of a Pulumi program with the Google Cloud provider, rather than creating them by hand. Use the gcp.cloudbuild.Trigger resource for the triggers and gcp.cloudbuildv2.Repository for the repository connection; each resource’s Registry page has usage examples in every supported language. Managing triggers this way keeps your CI/CD configuration versioned and reviewed alongside the rest of your infrastructure.
Speed up builds with caching
Cloud Build starts each build on a clean worker and keeps nothing between builds, so by default Pulumi re-downloads its provider plugins on every run. Cloud Build has no built-in cross-build cache, but you can avoid the repeated downloads in one of two ways.
Bake plugins into a custom builder image
Build a custom image from the official Pulumi image for your language and pre-install the provider plugins your program uses. The plugins are baked into the image, so no build step downloads them at run time:
FROM pulumi/pulumi-nodejs:latest
# Pre-install each provider plugin your program uses, pinned to the version
# from your dependency lockfile — for example, the Google Cloud provider:
RUN pulumi plugin install resource gcp 9.0.0
Push the image to Artifact Registry and reference it as the step name in place of pulumi/pulumi-nodejs. Rebuild the image whenever you change a provider version. This is the simplest and most deterministic option, and it’s the recommended approach for CI/CD.
Cache the plugins directory in a Cloud Storage bucket
If your set of providers changes often, cache Pulumi’s plugin directory in a Cloud Storage bucket instead. Cloud Build persists only the /workspace directory between steps, so point PULUMI_HOME at a path under /workspace, then add a step before the Pulumi step to restore the cache and a step after it to save the cache back:
steps:
# Restore the plugin cache, if any.
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- 'gcloud storage rsync --recursive gs://my-cache-bucket/pulumi-plugins /workspace/.pulumi/plugins || true'
- name: 'pulumi/pulumi-nodejs'
dir: 'infra'
entrypoint: 'bash'
env: ['PULUMI_HOME=/workspace/.pulumi']
args:
- '-c'
- |
npm install
pulumi preview --stack acme/website/staging
secretEnv: ['PULUMI_ACCESS_TOKEN']
# Save the plugin cache for the next build.
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- 'gcloud storage rsync --recursive /workspace/.pulumi/plugins gs://my-cache-bucket/pulumi-plugins'
The availableSecrets and options blocks are unchanged from the build configurations shown earlier. Set a lifecycle rule on the bucket so stale plugin versions don’t accumulate.
Additional resources
- Continuous delivery — overview of running Pulumi in CI/CD.
- Pulumi ESC — deliver credentials, secrets, and configuration to pipelines and developers consistently.
- OIDC issuers — exchange a CI/CD system’s OIDC token for a short-lived Pulumi access token.
- Version control integrations — pull request comments, status checks, and commit linking from Pulumi Cloud.
- Google Cloud provider — manage Cloud Build triggers, repository connections, and the rest of Google Cloud as code.
- Review Stacks — ephemeral environments created automatically for each pull request.
- CI/CD troubleshooting — diagnose common failures when running Pulumi in a pipeline.
Thank you for your feedback!
If you have a question about how to use Pulumi, reach out in Community Slack.
Open an issue on GitHub to report a problem or suggest an improvement.