Using GitLab CI/CD with Pulumi
GitLab CI/CD is the CI/CD service built into GitLab. It runs pipelines defined in a .gitlab-ci.yml file at the root of your repository, triggered by events such as merge requests, pushes, and tags.
You run Pulumi in a pipeline by invoking the Pulumi CLI directly. The official pulumi/pulumi container images ship the CLI together with a language runtime, so a job can run pulumi preview or pulumi up with no install step. Because the pipeline runs the CLI, it works with a Pulumi 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 GitLab project.
- A Pulumi program committed to that project. 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 — a long-lived Pulumi access token kept as a CI/CD variable. Simplest to set up.
- OIDC token exchange — no stored secret; the pipeline exchanges a short-lived OIDC token for a Pulumi access token at runtime. Recommended where you can use it.
Whichever you choose, 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 pipeline or a developer’s machine, a single environment definition works in both places — you don’t store separate cloud provider keys as CI/CD variables.
Authenticate with a stored access token
Your pipeline authenticates to Pulumi Cloud with a single Pulumi access token, supplied through the PULUMI_ACCESS_TOKEN environment variable. Prefer an organization or team token over a personal token so the pipeline’s identity isn’t tied to an individual.
Add the token as a CI/CD variable named PULUMI_ACCESS_TOKEN under your project’s Settings > CI/CD > Variables. Mark it Masked so it doesn’t appear in job logs. The Pulumi CLI reads the variable from the environment automatically — no explicit pulumi login is required.
Authenticate without a stored token using OIDC
You can remove the static token entirely. GitLab CI/CD can issue a short-lived OpenID Connect (OIDC) id_token for a job. Register GitLab as a trusted OIDC issuer in Pulumi Cloud, and the job exchanges that id_token for a short-lived Pulumi access token at runtime — no long-lived credential is stored as a CI/CD variable.
The trust flows inbound: GitLab issues the id_token, and pulumi login --oidc-token exchanges it with Pulumi Cloud for an access token. A job requests the token with the id_tokens keyword and logs in before running Pulumi. Apply this by adding the id_tokens block and the pulumi login step to the .pulumi hidden job in the workflow below:
variables:
PULUMI_ORG: acme
# This replaces the `.pulumi` hidden job in the workflow below.
.pulumi:
id_tokens:
PULUMI_OIDC_TOKEN:
aud: urn:pulumi:org:$PULUMI_ORG
before_script:
- pulumi login --oidc-token "$PULUMI_OIDC_TOKEN" --oidc-org "$PULUMI_ORG"
- cd infra
- npm ci # replace with your language's dependency-install command
With OIDC, the pipeline needs no PULUMI_ACCESS_TOKEN CI/CD variable. For the full setup — registering the issuer and writing the authorization policy that controls which projects and branches may exchange a token — see Configuring OpenID Connect for GitLab and the central OIDC issuers reference.
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. A single .gitlab-ci.yml covers the whole flow with three jobs:
previewrunspulumi previewon every merge request, surfacing the proposed changes for review.deploy-stagingrunspulumi upagainst the staging stack when changes land onmain.deploy-productionrunspulumi upagainst the production stack when arelease-*tag is pushed.
GitLab rules decide which jobs run for a given pipeline. The examples assume a Pulumi program in an infra/ directory and stacks named acme/website/staging and acme/website/production. A hidden .pulumi job, reused through extends, holds the steps the three jobs share; only the image and the dependency-install command differ between languages:
# .gitlab-ci.yml
stages:
- preview
- deploy
default:
image:
name: pulumi/pulumi-nodejs:latest
entrypoint: [""]
variables:
PULUMI_STACK_STAGING: acme/website/staging
PULUMI_STACK_PRODUCTION: acme/website/production
# Shared setup: enter the program directory and install dependencies.
.pulumi:
before_script:
- cd infra
- npm ci
# Merge request: preview the proposed changes.
preview:
extends: .pulumi
stage: preview
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- pulumi preview --stack "$PULUMI_STACK_STAGING"
# Push to main: deploy to the staging environment.
deploy-staging:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
script:
- pulumi up --yes --stack "$PULUMI_STACK_STAGING"
# Release tag: promote to production.
deploy-production:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_TAG =~ /^release-/
environment:
name: production
script:
- pulumi up --yes --stack "$PULUMI_STACK_PRODUCTION"
# .gitlab-ci.yml
stages:
- preview
- deploy
default:
image:
name: pulumi/pulumi-python:latest
entrypoint: [""]
variables:
PULUMI_STACK_STAGING: acme/website/staging
PULUMI_STACK_PRODUCTION: acme/website/production
# Shared setup: enter the program directory and install dependencies.
.pulumi:
before_script:
- cd infra
- pip install -r requirements.txt
# Merge request: preview the proposed changes.
preview:
extends: .pulumi
stage: preview
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- pulumi preview --stack "$PULUMI_STACK_STAGING"
# Push to main: deploy to the staging environment.
deploy-staging:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
script:
- pulumi up --yes --stack "$PULUMI_STACK_STAGING"
# Release tag: promote to production.
deploy-production:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_TAG =~ /^release-/
environment:
name: production
script:
- pulumi up --yes --stack "$PULUMI_STACK_PRODUCTION"
# .gitlab-ci.yml
stages:
- preview
- deploy
default:
image:
name: pulumi/pulumi-go:latest
entrypoint: [""]
variables:
PULUMI_STACK_STAGING: acme/website/staging
PULUMI_STACK_PRODUCTION: acme/website/production
# Shared setup: enter the program directory and install dependencies.
.pulumi:
before_script:
- cd infra
- go mod download
# Merge request: preview the proposed changes.
preview:
extends: .pulumi
stage: preview
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- pulumi preview --stack "$PULUMI_STACK_STAGING"
# Push to main: deploy to the staging environment.
deploy-staging:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
script:
- pulumi up --yes --stack "$PULUMI_STACK_STAGING"
# Release tag: promote to production.
deploy-production:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_TAG =~ /^release-/
environment:
name: production
script:
- pulumi up --yes --stack "$PULUMI_STACK_PRODUCTION"
# .gitlab-ci.yml
stages:
- preview
- deploy
default:
image:
name: pulumi/pulumi-dotnet:latest
entrypoint: [""]
variables:
PULUMI_STACK_STAGING: acme/website/staging
PULUMI_STACK_PRODUCTION: acme/website/production
# Shared setup: enter the program directory.
# The .NET runtime restores and builds the project during the Pulumi run.
.pulumi:
before_script:
- cd infra
# Merge request: preview the proposed changes.
preview:
extends: .pulumi
stage: preview
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- pulumi preview --stack "$PULUMI_STACK_STAGING"
# Push to main: deploy to the staging environment.
deploy-staging:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
script:
- pulumi up --yes --stack "$PULUMI_STACK_STAGING"
# Release tag: promote to production.
deploy-production:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_TAG =~ /^release-/
environment:
name: production
script:
- pulumi up --yes --stack "$PULUMI_STACK_PRODUCTION"
# .gitlab-ci.yml
stages:
- preview
- deploy
default:
image:
name: pulumi/pulumi-java:latest
entrypoint: [""]
variables:
PULUMI_STACK_STAGING: acme/website/staging
PULUMI_STACK_PRODUCTION: acme/website/production
# Shared setup: enter the program directory.
# The Java runtime resolves and builds the project during the Pulumi run.
.pulumi:
before_script:
- cd infra
# Merge request: preview the proposed changes.
preview:
extends: .pulumi
stage: preview
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- pulumi preview --stack "$PULUMI_STACK_STAGING"
# Push to main: deploy to the staging environment.
deploy-staging:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
script:
- pulumi up --yes --stack "$PULUMI_STACK_STAGING"
# Release tag: promote to production.
deploy-production:
extends: .pulumi
stage: deploy
rules:
- if: $CI_COMMIT_TAG =~ /^release-/
environment:
name: production
script:
- pulumi up --yes --stack "$PULUMI_STACK_PRODUCTION"
The pulumi up --yes flag applies changes without an interactive confirmation prompt, which is required in a non-interactive pipeline. The environment keyword records each deployment against a GitLab environment, giving you a deployment history and a one-click rollback in the GitLab UI.
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 job with a Review Stack, which provisions an ephemeral stack for the merge request and destroys it when the merge request closes.
Report results to GitLab
When a pipeline runs pulumi preview on a merge request, you’ll usually want the proposed changes summarized on the merge request itself rather than buried in the job logs. The Pulumi GitLab integration does this: connect your GitLab group to Pulumi Cloud once, and Pulumi posts a summary of resource changes — with links to the Pulumi Cloud console — as a merge request comment, along with commit status checks. It works for every project in the group regardless of which CI/CD system runs Pulumi.
Speed up runs with caching
GitLab CI/CD starts each job in a fresh container, so Pulumi re-downloads its plugins and policy packs every time. GitLab’s cache can only store paths inside the project directory, so point PULUMI_HOME at a directory in the workspace and cache that:
variables:
PULUMI_HOME: $CI_PROJECT_DIR/.pulumi
cache:
key:
files:
- infra/package-lock.json
paths:
- .pulumi/plugins
- .pulumi/policies
Keying the cache on your dependency manifest rebuilds it when dependencies change; use the file appropriate to your language — package-lock.json, requirements.txt, go.sum, the .csproj, or pom.xml.
Serialize deployments
When commits land faster than a pipeline finishes, deployment jobs can overlap. Running two pulumi up jobs against the same stack at once causes one to fail on an update conflict. Assign deployment jobs a resource_group so GitLab runs them one at a time:
deploy-staging:
# ...
resource_group: staging
deploy-production:
# ...
resource_group: production
Jobs in the same resource group queue instead of running concurrently, while jobs in different groups — staging and production here — still run in parallel.
Managing GitLab with Pulumi
You can manage GitLab itself — projects, groups, branch protection rules, and CI/CD variables — as code with the GitLab provider in the Pulumi Registry. This lets you define the CI/CD variables and project settings this guide describes as part of a Pulumi program.
Additional resources
- Continuous delivery — overview of running Pulumi in CI/CD.
- Pulumi GitLab integration — merge request comments, commit statuses, and review stacks from Pulumi Cloud.
- Configuring OpenID Connect for GitLab — register GitLab as a trusted OIDC issuer.
- OIDC issuers — exchange a CI/CD system’s OIDC token for a short-lived Pulumi access token.
- Pulumi ESC — deliver credentials, secrets, and configuration to pipelines and developers consistently.
- Review Stacks — ephemeral environments created automatically for each merge 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.