Skip to main content
  1. Docs
  2. Infrastructure as Code
  3. Operations
  4. Continuous Delivery
  5. Buildkite

Using Buildkite with Pulumi

    Buildkite is a CI/CD platform that runs pipelines on agents you host or on Buildkite-hosted agents. You run Pulumi in a Buildkite pipeline with the pulumi plugin, an official Buildkite plugin that installs and configures the Pulumi CLI for a step.

    Because the plugin runs CLI commands, it works with a Pulumi program written in any supported language, and with any cloud provider Pulumi supports.

    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:

    1. A Pulumi Cloud account and organization.
    2. A Buildkite account with a cluster and agent.
    3. A Git repository connected to a Buildkite pipeline.
    4. A Pulumi program. If you don’t have one yet, follow a Get started guide.

    Install and configure Pulumi

    A Buildkite pipeline’s steps can be defined in the web interface or, more commonly, in a pipeline.yml file committed to your repository. The Buildkite agent looks for this file in a few locations; .buildkite/pipeline.yml in the repository root is the conventional choice.

    Add the pulumi plugin to any step that runs a Pulumi command. The plugin installs the CLI before the step’s commands run:

    # .buildkite/pipeline.yml
    steps:
      - label: ":pulumi: Preview"
        commands:
          - cd infra && npm install
          - pulumi preview --stack acme/website/dev
        plugins:
          - pulumi#v1.0.0:
              # Pin a specific CLI version. Optional; defaults to the latest release.
              version: 3.143.0
    

    As an alternative to the plugin, you can run your step inside one of Pulumi’s official container images, which ship the Pulumi CLI and a language runtime preinstalled. Use the docker plugin to select the image for your language:

        plugins:
          - docker#v5.9.0:
              image: pulumi/pulumi-nodejs
              mount-buildkite-agent: true
              environment:
                - PULUMI_ACCESS_TOKEN
    

    Authenticate with Pulumi Cloud

    When your pipeline uses Pulumi Cloud as its backend, it needs only a single Pulumi access token to operate.

    Store the token as a Buildkite secret so it isn’t committed to source control, and use the secrets plugin to map it to the PULUMI_ACCESS_TOKEN environment variable:

        plugins:
          - secrets#v1.0.0:
              variables:
                PULUMI_ACCESS_TOKEN: PULUMI_ACCESS_TOKEN
          - pulumi#v1.0.0: ~
    

    Prefer an organization or team token over a personal token.

    You can remove even that static secret with OpenID Connect (OIDC). Register Buildkite as an OIDC issuer in Pulumi Cloud, then enable OIDC on the plugin — it exchanges the OIDC token Buildkite issues for the job for a short-lived Pulumi access token, so no long-lived credential is stored:

        plugins:
          - pulumi#v1.0.0:
              use-oidc: true
              audience: "urn:pulumi:org:ACME_ORG"
    

    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 in the pipeline.

    The trunk-based development workflow

    The most common way to run Pulumi in Buildkite follows a trunk-based development model, where work merges into a single main branch and deployments flow outward from there:

    1. Open a pull request. The pipeline runs pulumi preview and surfaces the proposed changes for review.
    2. Merge to the main branch. The pipeline runs pulumi up to deploy the change to an environment that receives continuous delivery, such as a shared development or staging environment.
    3. Promote to production. Pushing a git tag triggers a deployment to production, keeping production updates deliberate and traceable.

    The pipeline below implements all three stages with conditional steps. It assumes a Pulumi program in an infra/ directory and stacks named acme/website/staging and acme/website/production.

    # .buildkite/pipeline.yml
    steps:
      # Pull request: preview the proposed changes and post them as a build annotation.
      - label: ":pulumi: Preview"
        if: build.pull_request.id != null
        commands:
          - cd infra && npm install
          - pulumi preview --stack acme/website/staging 2>&1 | tee preview.txt
          - printf '```\n%s\n```\n' "$(cat preview.txt)" | buildkite-agent annotate --style info
        plugins:
          - secrets#v1.0.0:
              variables:
                PULUMI_ACCESS_TOKEN: PULUMI_ACCESS_TOKEN
          - pulumi#v1.0.0: ~
    
      # Merge to the main branch: deploy to the staging environment.
      - label: ":pulumi: Deploy to staging"
        if: build.branch == "main" && build.pull_request.id == null
        commands:
          - cd infra && npm install
          - pulumi up --yes --stack acme/website/staging
        plugins:
          - secrets#v1.0.0:
              variables:
                PULUMI_ACCESS_TOKEN: PULUMI_ACCESS_TOKEN
          - pulumi#v1.0.0: ~
    
      # Tag push: promote to production.
      - label: ":pulumi: Deploy to production"
        if: build.tag != null
        commands:
          - cd infra && npm install
          - pulumi up --yes --stack acme/website/production
        plugins:
          - secrets#v1.0.0:
              variables:
                PULUMI_ACCESS_TOKEN: PULUMI_ACCESS_TOKEN
          - pulumi#v1.0.0: ~
    

    To promote a release, push a tag:

    git tag release-2026-05-20
    git push origin release-2026-05-20
    

    Buildkite decides when to start a build — on pull requests, branch pushes, and tags — through your pipeline’s source control settings, not the pipeline file. Enable pull request and tag builds there; the if expressions above then select which steps run for each build.

    For an optional ephemeral environment on each pull request, pair the preview step with a Review Stack, which provisions and tears down a per-PR environment automatically.

    The Pulumi CLI automatically detects when it runs inside Buildkite and records the build and commit metadata. Each update in Pulumi Cloud then links back to the build and pull request that triggered it — no extra configuration required.

    Buildkite features worth knowing

    • Dynamic pipelines. Buildkite can generate pipeline steps at build time with dynamic pipelines. This is useful when you want to fan out a step per Pulumi stack or project rather than hard-coding each one.
    • Cache volumes. On hosted agents, cache volumes persist directories across builds. Caching ~/.pulumi/plugins and your language packages (node_modules, and so on) speeds up runs. Pulumi plugin versions are tied 1:1 to the Pulumi package versions that use them, so the cache stays correct as long as those packages are unchanged.

    Managing Buildkite with Pulumi

    You can also manage Buildkite itself — pipelines, teams, and agent tokens — as code with the community-maintained Pulumiverse Buildkite provider in the Pulumi Registry.

    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.
    • Review Stacks — ephemeral environments created automatically for each pull request.