Using Argo CD with Pulumi
Argo CD is a declarative, pull-based GitOps continuous delivery tool for Kubernetes. Pulumi integrates with Argo CD through the Pulumi Kubernetes Operator (PKO), which lets Argo CD manage Pulumi infrastructure the same way it manages any other Kubernetes manifest. This means you can use Argo CD to provision and update cloud resources beyond the Kubernetes API—including the clusters themselves.
How Pulumi works with Argo CD
Argo CD does not run pulumi commands directly. Instead, Pulumi infrastructure is represented as a Stack custom resource—a Kubernetes manifest that the Pulumi Kubernetes Operator knows how to reconcile.
Stack custom resource is not the same thing as a Pulumi stack. A Pulumi stack is an isolated instance of a Pulumi program, identified as organization/project/stack. The Stack custom resource is a Kubernetes object that tells PKO which Pulumi stack to deploy and how—each one targets a single Pulumi stack through its spec.stack field.The integration relies on two pieces:
- Argo CD syncs
Stackmanifests from Git to your cluster, like any other Kubernetes resource. - The Pulumi Kubernetes Operator watches for
Stackobjects and runs the Pulumi deployment in a dedicated workspace pod.
A change flows through the system like this:
- You commit a change to a
Stackmanifest (or to the Pulumi program it points at). - Argo CD detects the change in Git and syncs the
Stackobject to the cluster. - PKO reconciles the
Stack, runningpulumi upin a workspace pod. - PKO reports the result back through the
Stackobject’s status, which Argo CD surfaces in its UI.
Because the deployment runs inside the operator, there is no pipeline step that invokes the Pulumi CLI. Argo CD’s role is to keep the desired Stack specification in sync with Git.
Prerequisites
Before you begin, make sure you have:
- A Kubernetes cluster with Argo CD installed.
- The Pulumi Kubernetes Operator installed in the cluster.
- A Pulumi Cloud account and organization.
- A Git repository containing your Pulumi program.
- A Git repository (or a directory within an existing repository) for the Kubernetes manifests that Argo CD will sync.
This guide assumes you are using Pulumi Cloud. PKO also supports self-managed state backends through the Stack resource’s spec.backend field—see States & backends for details.
Authenticate with Pulumi Cloud
Your cluster needs a Pulumi Cloud identity. Give it one in one of two ways. Choose one — you don’t need both:
- OIDC token exchange — no stored secret; PKO workspace pods exchange their projected service account tokens for short-lived Pulumi access tokens. Recommended.
- A static access token — a long-lived Pulumi access token stored in a Kubernetes Secret.
Whichever you choose, Pulumi ESC (Environments, Secrets, and Configuration) then delivers cloud credentials, secrets, and configuration to every Stack consistently, so you don’t have to store separate cloud provider keys in the cluster for each stack.
Eliminate static tokens with OIDC
The recommended way to give the cluster its Pulumi Cloud identity is OpenID Connect (OIDC). Register the Kubernetes cluster as a Pulumi Cloud OIDC Issuer, and PKO workspace pods exchange their projected service account tokens for short-lived Pulumi access tokens. No long-lived PULUMI_ACCESS_TOKEN secret is stored in the cluster.
See Configuring OpenID Connect for Amazon EKS or Configuring OpenID Connect for Google Kubernetes Engine for setup steps. Once the issuer is configured, the Stack manifests in this guide need no envRefs.PULUMI_ACCESS_TOKEN block.
Use a static access token (alternative)
For clusters that are not registered as OIDC issuers, store a Pulumi access token in a Kubernetes Secret and reference it from the Stack. Prefer an organization or team token over a personal token:
kubectl create secret generic pulumi-access-token \
--from-literal=token=$PULUMI_ACCESS_TOKEN \
-n pulumi
Reference the secret with spec.envRefs:
spec:
envRefs:
PULUMI_ACCESS_TOKEN:
type: Secret
secret:
name: pulumi-access-token
key: token
Attach an ESC environment
Once the cluster is authenticated, use the spec.environment field on a Stack to attach one or more ESC environment names. The configuration and secrets from those environments—including dynamically brokered, short-lived cloud credentials—become available to your Pulumi program automatically:
apiVersion: pulumi.com/v1
kind: Stack
metadata:
name: webapp-staging
namespace: pulumi
spec:
serviceAccountName: webapp
stack: myorg/webapp/staging
projectRepo: https://github.com/myorg/pulumi-webapp.git
branch: main
environment:
- aws-credentials
- shared-config
Define a Stack custom resource
A Stack custom resource tells PKO which Pulumi stack to deploy, where the program lives, and how to run it. The example below also declares a service account and the cluster role bindings the deployment needs to create resources in the cluster.
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: webapp
namespace: pulumi
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: webapp:system:auth-delegator
annotations:
argocd.argoproj.io/sync-wave: "1"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: webapp
namespace: pulumi
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: webapp:cluster-admin
annotations:
argocd.argoproj.io/sync-wave: "1"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: webapp
namespace: pulumi
---
apiVersion: pulumi.com/v1
kind: Stack
metadata:
name: webapp-dev
namespace: pulumi
annotations:
argocd.argoproj.io/sync-wave: "2"
pulumi.com/reconciliation-request: "before-first-update"
link.argocd.argoproj.io/external-link: https://app.pulumi.com/myorg/webapp/dev
spec:
serviceAccountName: webapp
stack: myorg/webapp/dev
projectRepo: https://github.com/myorg/pulumi-webapp.git
branch: main
refresh: true
destroyOnFinalize: true
environment:
- aws-credentials
config:
webapp:replicas: "2"
The key spec fields are:
serviceAccountName: the service account the workspace pod runs as.stack: the fully qualified Pulumi stack name, inorganization/project/stackform.projectRepoandbranch: the Git location of the Pulumi program PKO executes. Usecommitinstead ofbranchto pin an exact revision.refresh: refreshes Pulumi state before each update so it reflects the real state of your infrastructure.destroyOnFinalize: runspulumi destroywhen theStackobject is deleted, so removing the manifest from Git tears the infrastructure down.
The Pulumi program referenced by projectRepo can be written in any supported language—TypeScript, Python, Go, .NET, Java, or YAML. The Stack manifest itself is language-agnostic, so this guide uses a single set of YAML examples.
cluster-admin binding above is shown for simplicity. For production, grant the service account only the permissions its Pulumi program actually needs.Create an Argo CD Application
An Argo CD Application tells Argo CD which Git directory to sync and where to apply the resulting manifests. Point its source.path at the directory containing your Stack manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: webapp-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io/background
spec:
project: default
source:
repoURL: https://github.com/myorg/webapp-manifests.git
targetRevision: main
path: manifests/dev
destination:
server: https://kubernetes.default.svc
namespace: pulumi
syncPolicy:
automated:
prune: true
selfHeal: true
The syncPolicy.automated block keeps the cluster in sync with Git without manual intervention: prune removes resources deleted from Git, and selfHeal reverts out-of-band changes. The resources-finalizer.argocd.argoproj.io/background finalizer pairs with destroyOnFinalize on the Stack—when the Application is deleted, Argo CD removes the Stack object in the background, which triggers PKO to destroy the infrastructure.
Build a trunk-based GitOps workflow
In trunk-based development, contributors merge small changes into a single main branch frequently. Because Argo CD reconciles Stack manifests from Git rather than running pipeline steps, each environment is represented by its own Stack manifest in its own directory, watched by its own Application. Promoting a change means changing which Git ref a Stack tracks—not running a deploy command.
Preview infrastructure changes in a pull request
When a pull request is opened against a Pulumi program, you want a dry run rather than a deployment. Add a Stack manifest with spec.preview: true and point spec.branch at the PR’s feature branch:
apiVersion: pulumi.com/v1
kind: Stack
metadata:
name: webapp-preview
namespace: pulumi
spec:
serviceAccountName: webapp
stack: myorg/webapp/preview
projectRepo: https://github.com/myorg/pulumi-webapp.git
branch: feature/add-cache
preview: true
environment:
- aws-credentials
PKO runs pulumi preview instead of pulumi up. The Stack status surfaces the preview link and program outputs without changing any infrastructure, and Argo CD shows the preview Stack as healthy once the dry run succeeds. Point the preview Stack at a dedicated Pulumi stack (myorg/webapp/preview above) to avoid state contention with your real environments. See Preview mode in the PKO documentation for more detail.
Deploy to dev or staging on merge to main
Your dev or staging Stack tracks the main branch:
spec:
stack: myorg/webapp/staging
branch: main
When a pull request merges, PKO’s branch polling detects the new commit on main and runs pulumi up against the staging environment. Tune the polling interval with spec.resyncFrequencySeconds, or trigger an immediate Argo CD sync.
Promote to production with a release branch
Production should not track main directly. PKO’s spec.branch field takes a branch reference, so the Argo CD equivalent of a moving release tag is a long-lived release branch that you fast-forward to a vetted commit to promote:
spec:
stack: myorg/webapp/production
branch: release
To promote, advance the release branch to the commit you have validated in staging and push it. PKO reconciles production to that commit on its next sync.
If you need fully immutable, auditable releases, pin spec.commit to an exact SHA and update it for each promotion instead:
spec:
stack: myorg/webapp/production
commit: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
A moving release branch makes promotion a single Git operation; a pinned commit records exactly which revision is live. Either way, promotion is an explicit, reviewable change in Git.
A typical repository layout for this workflow keeps one directory per environment:
webapp-manifests/
├── manifests/
│ ├── preview/
│ │ └── stack.yaml # tracks the PR branch, preview: true
│ ├── staging/
│ │ └── stack.yaml # tracks main
│ └── production/
│ └── stack.yaml # tracks the release branch
└── applications/
├── preview.yaml # Argo CD Application for manifests/preview
├── staging.yaml # Argo CD Application for manifests/staging
└── production.yaml # Argo CD Application for manifests/production
Order deployments with sync waves
Argo CD sync waves control the order in which resources are applied. Annotate resources with argocd.argoproj.io/sync-wave; lower numbers are applied first. The Stack custom resource above uses waves to apply the RBAC resources (wave 1) before the Stack itself (wave 2):
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1"
For dependencies between Pulumi stacks—for example, creating a cluster before deploying applications into it—use the Stack resource’s own spec.prerequisites field, which lets one Stack wait for another to succeed. See Stack Prerequisites in the PKO documentation.
Monitor deployments
Link to Pulumi Cloud: The
link.argocd.argoproj.io/external-linkannotation adds a link from the Argo CD UI directly to the stack in Pulumi Cloud, where you can see detailed deployment information:metadata: annotations: link.argocd.argoproj.io/external-link: https://app.pulumi.com/myorg/webapp/devHealth status: PKO ships custom Argo CD health checks for the
Stackresource, so Argo CD reports an accurateHealthy,Progressing, orDegradedstatus that reflects the underlying Pulumi deployment.Force a sync: The
pulumi.com/reconciliation-requestannotation triggers PKO to reconcile theStack. Setting it to a new value—"before-first-update"for the initial deployment, or any unique string afterward—requests a fresh update.
Troubleshooting
Stack deployment fails
- Check the
Stackobject’s status in the Argo CD UI or withkubectl describe stack <name> -n pulumi. - PKO runs each deployment in a workspace pod. List the pods with
kubectl get pods -n pulumiand inspect the logs of the one for the failing stack withkubectl logs <pod-name> -n pulumi. - Verify that the cluster can authenticate to Pulumi Cloud—confirm the OIDC issuer is configured, or that the access token secret exists and has the required permissions.
Argo CD shows the Stack as Unknown or Progressing
- PKO provides custom health checks for
Stackresources. AProgressingstatus means the deployment is still in flight; if it persists, check the workspace pod logs. - Check whether the
Stackis waiting on a prerequisite to be satisfied. - Verify that the referenced Git repository and path are accessible from the cluster.
Stack stuck in an updating state
- Check for resource conflicts or locks in your cloud provider.
- Review the workspace pod logs for the stack.
- Enable
refresh: trueso PKO reconciles Pulumi state with the real state of your infrastructure before each update.
Additional resources
- Pulumi Kubernetes Operator — the operator that reconciles Pulumi stacks from inside your cluster.
- Pulumi ESC — deliver credentials, secrets, and configuration to stacks and developers consistently.
- OIDC issuers — exchange a cluster’s OIDC token for a short-lived Pulumi access token.
- Kubernetes provider — manage Kubernetes resources with Pulumi.
- Continuous delivery — overview of running Pulumi in CI/CD.
- Argo CD documentation — official Argo CD project documentation.
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.