<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0"><channel><title>Pulumi Blog: Best practices</title><link>https://www.pulumi.com/blog/tag/best-practices/</link><description>Pulumi blog posts: Best practices.</description><language>en-us</language><pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate><item><title>GitOps Best Practices I Wish I Had Known Before</title><link>https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/</link><pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/gitops-best-practices-i-wish-i-had-known-before/index.png" /&gt;
&lt;p&gt;Getting started with GitOps can feel like trying to herd cats through a YAML factory while the factory is on fire. It&amp;rsquo;s one of those things that seems like it ought to be simple (just use Git!), but in practice is much more complex — and you may not realize how much more complex until you&amp;rsquo;re weeks or more into a project. After years of running GitOps workflows in production across dozens of clusters, I&amp;rsquo;ve collected a list of best practices that I&amp;rsquo;m hoping can save you from having to make many of the mistakes I&amp;rsquo;ve made. Think of it as the GitOps cheat sheet I wish I&amp;rsquo;d had from Day 1.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
Since this post was published, Pulumi has added first-class support for HashiCorp
Configuration Language (HCL). You can now write Pulumi programs in HCL directly, alongside
general-purpose languages like TypeScript, Python, Go, and C#. To see how it works, see
&lt;a href="https://www.pulumi.com/docs/iac/languages-sdks/hcl/"&gt;Pulumi HCL&lt;/a&gt;.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you&amp;rsquo;re not familiar with the formal definition, the &lt;a href="https://opengitops.dev/"&gt;OpenGitOps&lt;/a&gt; project distills it into four principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Declarative desired state&lt;/li&gt;
&lt;li&gt;Versioned and immutable storage&lt;/li&gt;
&lt;li&gt;Automatic pulling&lt;/li&gt;
&lt;li&gt;Continuous reconciliation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But those principles only define the &lt;em&gt;what&lt;/em&gt;. This post is also about the &lt;em&gt;how&lt;/em&gt; — the practical lessons that can make or break a GitOps implementation.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;rsquo;ll walk you through the GitOps best practices I&amp;rsquo;ve picked up from production experience, community talks, and more than a few late-night incident calls. Whether you&amp;rsquo;re just getting started with GitOps or looking to level up, these tips should help you avoid the potholes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#1-git-is-your-single-source-of-truth-no-really"&gt;Git is your single source of truth (no, really)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#2-declarative-over-imperative-always"&gt;Declarative over imperative, always&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#3-pull-based-deployments-are-the-way"&gt;Pull-based deployments are the way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#4-separate-app-code-from-deployment-config-when-it-hurts-not-to"&gt;Separate app code from deployment config (when it hurts not to)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#5-use-directories-not-branches-for-environments"&gt;Use directories, not branches, for environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#6-validate-before-you-merge"&gt;Validate before you merge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#7-tag-with-commit-shas-not-latest"&gt;Tag with commit SHAs, not &amp;ldquo;latest&amp;rdquo;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#8-automate-drift-detection-and-reconciliation"&gt;Automate drift detection and reconciliation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#9-practice-progressive-delivery"&gt;Practice progressive delivery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#10-policy-as-code-your-automated-guardrails"&gt;Policy-as-code: your automated guardrails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#11-bridge-your-iac-and-gitops-dont-choose-one"&gt;Bridge your IaC and GitOps (don&amp;rsquo;t choose one)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/#12-be-pragmatic-not-dogmatic"&gt;Be pragmatic, not dogmatic&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img src="https://www.pulumi.com/blog/gitops-best-practices-i-wish-i-had-known-before/gitops-meme.png"
alt="Drake meme: rejecting kubectl apply, approving git push" width="100%"&gt;&lt;figcaption&gt;
&lt;p&gt;The GitOps mindset in a nutshell.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="1-git-is-your-single-source-of-truth-no-really"&gt;1. Git is your single source of truth (no, really)&lt;/h2&gt;
&lt;p&gt;This is the bedrock that the principles of GitOps are built on: every piece of your environment&amp;rsquo;s desired state lives in a Git repository. No exceptions. No &amp;ldquo;I&amp;rsquo;ll just fix it real quick with &lt;code&gt;kubectl edit&lt;/code&gt;.&amp;rdquo; No &amp;ldquo;let me patch this configmap by hand because the PR process takes too long.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The moment you make a manual change to your cluster, you&amp;rsquo;ve created drift between what Git says the world should look like and what it actually looks like. Drift will quietly wreck your GitOps workflows if you let it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Put everything in Git: Kubernetes manifests, Helm values, Kustomize overlays, policy rules, even your GitOps tool configuration itself.&lt;/li&gt;
&lt;li&gt;No manual &lt;code&gt;kubectl&lt;/code&gt; edits. If it&amp;rsquo;s not in Git, it doesn&amp;rsquo;t exist. Period. Train your team to treat direct cluster changes like touching a hot stove.&lt;/li&gt;
&lt;li&gt;You get an audit trail for free. Git gives you a complete history of who changed what, when, and why. That&amp;rsquo;s your compliance audit trail baked right in.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Enable branch protection rules on your GitOps repos from Day 1. This prevents anyone from pushing directly to main and bypassing the review process. Future you will thank present you during the next audit.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="2-declarative-over-imperative-always"&gt;2. Declarative over imperative, always&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re still running sequences of &lt;code&gt;kubectl create&lt;/code&gt;, &lt;code&gt;kubectl patch&lt;/code&gt;, and &lt;code&gt;kubectl delete&lt;/code&gt; commands to manage your cluster, you&amp;rsquo;re not really doing GitOps yet. Declarative means you define &lt;em&gt;what&lt;/em&gt; you want the end state to look like, not the step-by-step instructions to get there.&lt;/p&gt;
&lt;p&gt;Think of it like ordering at a restaurant. Imperative: &amp;ldquo;Go to the kitchen, grab flour, knead dough, preheat the oven to 200 degrees, shape the pizza, add sauce&amp;hellip;&amp;rdquo; Declarative: &amp;ldquo;One margherita pizza, please.&amp;rdquo; Let the system figure out how to make it happen.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your manifests describe the end result. The GitOps operator reconciles reality to match.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s idempotent by design. Apply the same manifest ten times, get the same result. No side effects, no surprises.&lt;/li&gt;
&lt;li&gt;Rollbacks are easier: just revert a commit. The operator sees the previous desired state and reconciles. A caveat though: this only works for &lt;em&gt;stateless&lt;/em&gt; resources; database schema migrations, CRD version changes, persistent volume modifications, and rotated secrets don&amp;rsquo;t always revert cleanly by rolling back to a previous commit. Be sure to plan rollbacks involving stateful resources carefully.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: If you find yourself writing shell scripts that run a sequence of &lt;code&gt;kubectl&lt;/code&gt; commands, stop and ask yourself: &amp;ldquo;Can I express this as a declarative manifest instead?&amp;rdquo; Nine times out of ten, the answer is yes.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="3-pull-based-deployments-are-the-way"&gt;3. Pull-based deployments are the way&lt;/h2&gt;
&lt;p&gt;Traditional CI/CD is push-based: your pipeline builds an artifact and then pushes it to the cluster. GitOps flips this. An agent running inside your cluster continuously polls a Git repository and pulls changes when it detects them.&lt;/p&gt;
&lt;p&gt;Why does this matter? With push-based deployments, your CI system needs credentials to access your cluster. That&amp;rsquo;s a wide attack surface. With pull-based, the agent already lives inside the cluster and only needs read access to your Git repo.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools like &lt;a href="https://argoproj.github.io/cd/"&gt;ArgoCD&lt;/a&gt; and &lt;a href="https://fluxcd.io/"&gt;FluxCD&lt;/a&gt; run controllers inside your cluster that watch your Git repos.&lt;/li&gt;
&lt;li&gt;Your CI pipeline never needs &lt;code&gt;kubeconfig&lt;/code&gt; access. The agent handles deployment.&lt;/li&gt;
&lt;li&gt;The agent doesn&amp;rsquo;t just deploy once; it continuously ensures the cluster matches the desired state in Git.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;&lt;p&gt;Pro Tip: You may be tempted to set your reconciliation interval to something aggressive like 1 minute so you always know exactly which version is deployed. That works for a while, but at scale (200+ applications polling every minute) you can blow through GitHub&amp;rsquo;s API rate limits (5,000 requests/hour for authenticated users) and put real pressure on the Kubernetes API server.&lt;/p&gt;
&lt;p&gt;A better approach: set up &lt;a href="https://argo-cd.readthedocs.io/en/latest/operator-manual/webhook/"&gt;webhook receivers&lt;/a&gt;. Both ArgoCD and &lt;a href="https://fluxcd.io/flux/guides/webhook-receivers/"&gt;FluxCD&lt;/a&gt; support incoming webhooks from GitHub, GitLab, and Bitbucket. Your Git provider pings the GitOps controller on every push, so reconciliation kicks off in seconds instead of waiting for the next poll. That alone kills most of the API rate limit pain at scale. You can then relax polling to a 5 or 10 minute fallback for the rare case where a webhook doesn&amp;rsquo;t fire.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="4-separate-app-code-from-deployment-config-when-it-hurts-not-to"&gt;4. Separate app code from deployment config (when it hurts not to)&lt;/h2&gt;
&lt;p&gt;Let me be upfront: if you&amp;rsquo;re a small team with one or two services, keeping your Kubernetes manifests next to your application source code in the same repo is fine. A monorepo is simpler to manage and one less thing to automate. Don&amp;rsquo;t split repos just because a blog post told you to.&lt;/p&gt;
&lt;p&gt;That said, &lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/"&gt;ArgoCD&amp;rsquo;s official best practices&lt;/a&gt; call separate repos &amp;ldquo;highly recommended,&amp;rdquo; and there are real reasons for that once you grow. Application code and deployment configuration have different lifecycles. You might bump resource limits in a Helm values file without touching a single line of app code. Or you might refactor your entire codebase without changing any deployment parameters. In a shared repo, every config tweak triggers your full CI pipeline, and ArgoCD invalidates the manifest cache for all applications in the repo on every commit, not just the ones that changed. That cache invalidation alone can become a performance problem with dozens of apps in one repo.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When config tweaks start blocking app code from getting through CI, it may be time to split.&lt;/li&gt;
&lt;li&gt;When different teams need different access levels (not everyone who pushes app code should modify production deployment config), it&amp;rsquo;s time to split.&lt;/li&gt;
&lt;li&gt;When you&amp;rsquo;re running microservices with independent release cadences that step on each other, it&amp;rsquo;s time to split.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If none of those apply yet, don&amp;rsquo;t split. You&amp;rsquo;ll know when the pain arrives.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: When you do split, a common pattern is two repos: one for application source code, one for deployment configuration (Helm charts, Kustomize overlays, environment-specific values). Some larger orgs go further with a third repo for environment overrides, following the &lt;a href="https://fluxcd.io/flux/guides/repository-structure/"&gt;fleet repo model&lt;/a&gt; from the FluxCD maintainers. Either way, pair it with image update automation (&lt;a href="https://fluxcd.io/flux/components/image/"&gt;Flux Image Automation Controller&lt;/a&gt; or &lt;a href="https://argocd-image-updater.readthedocs.io/en/stable/"&gt;ArgoCD Image Updater&lt;/a&gt;) so image tag bumps across repos don&amp;rsquo;t turn into manual toil.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="5-use-directories-not-branches-for-environments"&gt;5. Use directories, not branches, for environments&lt;/h2&gt;
&lt;p&gt;This is the number one anti-pattern I encounter in the wild. Teams create a &lt;code&gt;dev&lt;/code&gt; branch, a &lt;code&gt;staging&lt;/code&gt; branch, and a &lt;code&gt;prod&lt;/code&gt; branch, then cherry-pick or merge between them for promotions. It sounds logical, but it&amp;rsquo;s a trap. Branches diverge, cherry-picks get missed, configs meant for &lt;code&gt;dev&lt;/code&gt; sneak into &lt;code&gt;staging&lt;/code&gt; or &lt;code&gt;prod&lt;/code&gt;, and before you know it, your environments have drifted, and you can&amp;rsquo;t tell what&amp;rsquo;s actually different between them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A folder structure like &lt;code&gt;environments/dev/&lt;/code&gt;, &lt;code&gt;environments/staging/&lt;/code&gt;, &lt;code&gt;environments/prod/&lt;/code&gt; makes differences visible in a single &lt;code&gt;diff&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;Moving a change from dev to prod is just copying or updating files across directories, reviewed via a standard pull request.&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ll never accidentally skip a commit or introduce an environment-specific change where it doesn&amp;rsquo;t belong. No more cherry-pick roulette.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing to note: the commonly used &lt;a href="https://akuity.io/blog/the-rendered-manifests-pattern"&gt;rendered manifests pattern&lt;/a&gt; actually &lt;em&gt;does&lt;/em&gt; use per-environment branches, but not in a human-managed way. In that pattern, config changes are still committed to &lt;code&gt;main&lt;/code&gt; (using named directories as above), and an automated CI process pushes environment YAML to read-only environment branches, which ArgoCD reads from. If you want to try it, ArgoCD&amp;rsquo;s built-in &lt;a href="https://argo-cd.readthedocs.io/en/latest/user-guide/source-hydrator/"&gt;source hydrator&lt;/a&gt; and &lt;a href="https://github.com/akuity/kargo-render"&gt;Kargo Render&lt;/a&gt; can automate the rendering for you.&lt;/p&gt;
&lt;p&gt;To keep things DRY, you can look to a tool like &lt;a href="https://kustomize.io/"&gt;Kustomize&lt;/a&gt;, which lets you use a shared base environment and then apply per-environment patches. Your base directory holds the common manifests, and each environment directory contains only the differences. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gitops-repo/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── base/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── deployment.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── service.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── kustomization.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── environments/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── dev/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── kustomization.yaml # replicas: 1, limits: 256Mi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── patches/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── staging/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── kustomization.yaml # replicas: 2, limits: 512Mi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── patches/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── prod/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── kustomization.yaml # replicas: 3, limits: 1Gi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── patches/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This minimizes duplication while keeping environment boundaries clear.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: If you&amp;rsquo;re working at the IaC layer, &lt;a href="https://www.pulumi.com/docs/iac/concepts/config/"&gt;Pulumi stack configuration&lt;/a&gt; solves a similar problem: each stack (dev, staging, prod) has its own config file with environment-specific values, while the program logic stays shared. It&amp;rsquo;s the same principle of &amp;ldquo;one source, per-environment overrides&amp;rdquo; applied above the Kubernetes manifest level.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="6-validate-before-you-merge"&gt;6. Validate before you merge&lt;/h2&gt;
&lt;p&gt;Catching errors after they&amp;rsquo;ve been applied to your cluster is expensive. Catching them in CI before the merge is cheap. Shift left as hard as you can.&lt;/p&gt;
&lt;p&gt;Your GitOps CI pipeline should validate everything it can before the change reaches your cluster: YAML syntax, Kubernetes schema validation, policy compliance, dry-run rendering.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools like &lt;a href="https://github.com/adrienverge/yamllint"&gt;&lt;code&gt;yamllint&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/yannh/kubeconform"&gt;&lt;code&gt;kubeconform&lt;/code&gt;&lt;/a&gt; catch syntax errors and schema violations before they become runtime failures. (Note: &lt;code&gt;kubeval&lt;/code&gt;, which you&amp;rsquo;ll see in older guides, is no longer maintained — its own README points to kubeconform as the replacement.)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;helm template&lt;/code&gt; or &lt;code&gt;kustomize build&lt;/code&gt; in CI to verify that your templates render without errors.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://www.openpolicyagent.org/"&gt;OPA&lt;/a&gt;/&lt;a href="https://www.conftest.dev/"&gt;Conftest&lt;/a&gt; or &lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; to enforce organizational policies (no privileged containers, required labels, resource limits set) before merge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s a minimal GitHub Actions workflow that validates your GitOps manifests on every PR:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Validate GitOps Manifests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;environments/**&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Validate YAML schemas&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;kubeconform --strict environments/**/*.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build and verify Kustomize output&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; for env in environments/*/; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; kustomize build &amp;#34;$env&amp;#34; &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; done&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Diff against live cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; kustomize build environments/staging/ | \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; kubectl diff -f - || true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Add a &lt;code&gt;kubectl diff&lt;/code&gt; step in your CI pipeline that shows exactly what would change in the cluster if the PR were merged. This gives reviewers a concrete view of the impact, not just the YAML diff.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="7-tag-with-commit-shas-not-latest"&gt;7. Tag with commit SHAs, not &amp;ldquo;latest&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Using &lt;code&gt;latest&lt;/code&gt; as your image tag in a GitOps workflow is playing fast and loose with your deployments. You have no idea what version is actually running, you can&amp;rsquo;t reliably roll back, and your Git history becomes meaningless because the same manifest could deploy completely different code depending on when it was synced.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tag your container images with the Git commit SHA that produced them. This creates a direct link between your source code, your build, and your deployment.&lt;/li&gt;
&lt;li&gt;Once a tag is pushed, it should never be overwritten. No re-tagging, no &amp;ldquo;oops let me push a fix to the same tag.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Need to roll back? Just revert the commit that updated the image tag. The previous SHA still points to the exact same image it always did.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Set up an admission controller or CI policy that rejects any manifest using &lt;code&gt;latest&lt;/code&gt; or any other mutable tag. Make it impossible to deploy without a pinned version.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="8-automate-drift-detection-and-reconciliation"&gt;8. Automate drift detection and reconciliation&lt;/h2&gt;
&lt;p&gt;Drift is when your cluster&amp;rsquo;s actual state doesn&amp;rsquo;t match the desired state in Git. It happens when someone runs a manual &lt;code&gt;kubectl&lt;/code&gt; command, when an autoscaler changes a replica count, or when a CRD controller modifies a resource behind your back.&lt;/p&gt;
&lt;p&gt;The whole point of GitOps is continuous reconciliation. Your operator should constantly compare the live state against Git and bring things back in line.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configure your GitOps tool to automatically revert manual changes, but build your exclusion lists (the set of fields and resources you intentionally skip during reconciliation) before you turn on auto-sync, not after. Controllers like Istio, cert-manager, and Crossplane legitimately modify resources, and auto-remediating those changes creates reconciliation loops that can destabilize a cluster. Start with a conservative exclusion list and tighten it over time.&lt;/li&gt;
&lt;li&gt;Even with auto-remediation, you want to know when drift happens. Set up alerts. It might indicate a process problem or a team member who needs coaching.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;&lt;p&gt;Pro Tip: Be precise about what you reconcile. HPAs, VPAs, cert-manager annotations, and external-dns records all legitimately modify resources. Use &lt;a href="https://argo-cd.readthedocs.io/en/latest/user-guide/diffing/"&gt;ignore rules&lt;/a&gt; to exclude fields that are intentionally dynamic. In ArgoCD, that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ignoreDifferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;apps&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deployment&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jsonPointers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/spec/replicas&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;admissionregistration.k8s.io&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;MutatingWebhookConfiguration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jqPathExpressions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;.webhooks[]?.clientConfig.caBundle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Also, don&amp;rsquo;t overlook resource ordering. &lt;a href="https://argo-cd.readthedocs.io/en/latest/user-guide/sync-waves/"&gt;ArgoCD sync waves&lt;/a&gt; and &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/#dependencies"&gt;Flux&amp;rsquo;s &lt;code&gt;dependsOn&lt;/code&gt;&lt;/a&gt; let you enforce that CRDs are applied before custom resources and namespaces before workloads. Getting ordering wrong is one of the most common causes of failed syncs in production.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="9-practice-progressive-delivery"&gt;9. Practice progressive delivery&lt;/h2&gt;
&lt;p&gt;Progressive delivery lets you gradually roll out changes, verify they&amp;rsquo;re working, and automatically roll back if something goes wrong.&lt;/p&gt;
&lt;p&gt;GitOps and progressive delivery fit well together. Your Git repo describes the desired rollout strategy, and controllers in the cluster execute it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canary deployments: send a small percentage of traffic to the new version. If error rates spike, automatically roll back.&lt;/li&gt;
&lt;li&gt;Blue-green deployments: run two identical environments, switch traffic once the new one is verified healthy.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://argoproj.github.io/rollouts/"&gt;Argo Rollouts&lt;/a&gt; extends Kubernetes with canary, blue-green, and analysis-driven rollout strategies that integrate with ArgoCD.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://flagger.app/"&gt;Flagger&lt;/a&gt; is the Flux ecosystem equivalent, with automated canary analysis, A/B testing, and blue-green deployments using service mesh or ingress controllers.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kargo.io/"&gt;Kargo&lt;/a&gt; takes it a step further by orchestrating promotions across multiple stages and environments. Instead of manually wiring up pipelines to move changes from dev to staging to prod, Kargo automates the entire promotion workflow on top of ArgoCD. I did a deep dive on GitOps promotion tools and why they belong in your toolkit:&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/OhFdLTU5sAo?rel=0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Combine progressive delivery with automated analysis. Tools like Argo Rollouts and Kargo can query Prometheus metrics during a canary and automatically promote or roll back based on error rates, latency percentiles, or custom metrics.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="10-policy-as-code-your-automated-guardrails"&gt;10. Policy-as-code: your automated guardrails&lt;/h2&gt;
&lt;p&gt;Reviews are great, but humans make mistakes. Policy-as-code adds an automated layer that enforces organizational rules before a change can land in your cluster. Think of it as guardrails on a mountain road: they don&amp;rsquo;t slow you down, they just keep you from going over the edge.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.openpolicyagent.org/"&gt;OPA&lt;/a&gt;/Gatekeeper lets you define policies in Rego that the admission controller enforces at deploy time (e.g., no containers running as root, all deployments must have resource limits).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; is a Kubernetes-native policy engine that uses familiar YAML to define and enforce policies, including mutation and generation.&lt;/li&gt;
&lt;li&gt;Starting with Kubernetes 1.26+, you can write &lt;a href="https://cel.dev/"&gt;CEL&lt;/a&gt;-based &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/"&gt;ValidatingAdmissionPolicies&lt;/a&gt; natively without any external tooling. A lightweight option if you don&amp;rsquo;t need the full feature set of OPA or Kyverno.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/iac/packages-and-automation/crossguard/"&gt;Pulumi CrossGuard&lt;/a&gt; lets you write policy packs in TypeScript or Python that validate infrastructure before it&amp;rsquo;s deployed. If your IaC and GitOps layers are bridged (see the next section), CrossGuard can enforce rules across both.&lt;/li&gt;
&lt;li&gt;Run policy checks in your CI pipeline so violations are caught during the PR, not after deployment.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Start with a small set of high-impact policies (required labels, no privileged containers, resource limits) and expand over time. Trying to enforce 50 policies on Day 1 will create so much friction that teams will revolt.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="rounded-lg bg-violet-50 p-6 my-8"&gt;
&lt;p class="heading-4 m-0 mb-3 flex items-center gap-1.5"&gt;Try infrastructure as code with Pulumi&lt;/p&gt;
&lt;div class="body-base m-0 text-gray-950"&gt;Define your cloud resources in TypeScript, Python, or Go, then pass stack outputs straight into your GitOps manifests so both layers stay in sync.&lt;/div&gt;
&lt;a href="https://app.pulumi.com/signup" data-track="blog-body-cta" class="btn btn-primary mt-4"&gt;
Get started
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--regular size-4" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-arrow-right-regular"/&gt;&lt;/svg&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id="11-bridge-your-iac-and-gitops-dont-choose-one"&gt;11. Bridge your IaC and GitOps (don&amp;rsquo;t choose one)&lt;/h2&gt;
&lt;p&gt;I hear this all the time: &amp;ldquo;We use Terraform for infrastructure and ArgoCD for deployments, and they live in completely separate worlds.&amp;rdquo; Sound familiar? Most teams treat IaC and GitOps as independent workflows, but they&amp;rsquo;re really two halves of the same story.&lt;/p&gt;
&lt;p&gt;IaC handles &amp;ldquo;Day 0,&amp;rdquo; creating the cloud resources your cluster depends on: VPCs, the Kubernetes cluster itself, IAM roles, databases, DNS zones. GitOps handles &amp;ldquo;Day 2,&amp;rdquo; managing what runs inside the cluster: applications, addons, configurations. The gap between them is where things get messy. Your Helm charts need IAM role ARNs. Your GitOps-deployed addons need to know the cluster&amp;rsquo;s OIDC provider endpoint. That metadata has to flow from the IaC layer to the GitOps layer somehow.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/gitops-bridge-dev/gitops-bridge"&gt;gitops-bridge&lt;/a&gt; pattern solves this cleanly: IaC provisions cloud resources and writes metadata (role ARNs, account IDs, endpoints) into Kubernetes resources (like ConfigMaps or Secrets) that your GitOps tool can consume directly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep each tool in its lane. Don&amp;rsquo;t force Terraform&amp;rsquo;s Helm provider to fight with ArgoCD over resource ownership.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/get-started/"&gt;Pulumi&lt;/a&gt; lets you define your cloud infrastructure in real programming languages (TypeScript, Python, Go), which makes it natural to compute and pass metadata to your GitOps layer. You can use &lt;a href="https://www.pulumi.com/docs/iac/concepts/stacks/#outputs"&gt;stack outputs&lt;/a&gt; to expose values like role ARNs and cluster endpoints, then feed them into your GitOps manifests. The &lt;a href="https://www.pulumi.com/docs/iac/guides/continuous-delivery/pulumi-kubernetes-operator/"&gt;Pulumi Kubernetes Operator&lt;/a&gt; even lets ArgoCD reconcile Pulumi stacks via GitOps.&lt;/li&gt;
&lt;li&gt;Statsig went from &amp;ldquo;1-2 devs clicking around cloud consoles with SEVs left and right&amp;rdquo; to fully self-service by using &lt;a href="https://www.statsig.com/blog/scaling-infra-with-pulumi-argocd"&gt;Pulumi to generate manifests and ArgoCD to deploy them&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;As you grow beyond a handful of clusters, think multi-cluster from the start. Patterns like &lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/"&gt;ArgoCD ApplicationSets&lt;/a&gt; with cluster generators and &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/#remote-clusters--cluster-api"&gt;Flux&amp;rsquo;s Kustomization targeting&lt;/a&gt; become important for fleet management. The gitops-bridge pattern works well here because your IaC layer can register new clusters and write their metadata into Kubernetes resources, which ApplicationSets automatically pick up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s how the gitops-bridge looks in practice. Pulumi provisions cloud resources and writes metadata into a ConfigMap that ArgoCD consumes:&lt;/p&gt;
&lt;div&gt;
&lt;pulumi-chooser type="language" options="typescript,python" mode=""&gt;
&lt;div&gt;
&lt;pulumi-choosable type="language" values="typescript" mode=""&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kr"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;@pulumi/kubernetes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clusterMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cluster-metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cluster-metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;argocd&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;gitops-bridge&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;aws_account_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;123456789012&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cluster_name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;prod-us-east-1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:iam::123456789012:role/app-role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;oidc_provider&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/pulumi-choosable&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pulumi-choosable type="language" values="python" mode=""&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pulumi_kubernetes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;k8s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cluster_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k8s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;cluster-metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k8s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ObjectMetaArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cluster-metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;argocd&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;gitops-bridge&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;aws_account_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;123456789012&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;cluster_name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;prod-us-east-1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;iam_role_arn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:iam::123456789012:role/app-role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;oidc_provider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/pulumi-choosable&gt;
&lt;/div&gt;
&lt;/pulumi-chooser&gt;
&lt;/div&gt;
&lt;p&gt;Your ArgoCD Applications or Helm values can then reference this ConfigMap, closing the loop between IaC and GitOps without hardcoding cloud-specific values.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: If you&amp;rsquo;re using Terraform today, the gitops-bridge pattern works there too. But if you&amp;rsquo;re tired of wrangling HCL and want type-safe infrastructure code with real programming constructs, &lt;a href="https://www.pulumi.com/docs/get-started/"&gt;give Pulumi a look&lt;/a&gt;. The bridge from IaC to GitOps becomes much more natural when both sides speak a real programming language.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="12-be-pragmatic-not-dogmatic"&gt;12. Be pragmatic, not dogmatic&lt;/h2&gt;
&lt;p&gt;Michael Crenshaw from Intuit &lt;a href="https://www.youtube.com/watch?v=m9a36_oEXa4"&gt;gave a talk titled &amp;ldquo;How GitOps Should I Be?&amp;rdquo;&lt;/a&gt; at a CNCF conference. Intuit runs 45 ArgoCD instances managing 20,000 applications across 200 clusters. The approach they took: &amp;ldquo;Be strategic, be pragmatic. GitOps most of the time.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;One of the largest GitOps adopters in the world explicitly deviates from pure GitOps when the situation calls for it. Dogmatic adherence to principles that don&amp;rsquo;t serve you is just another form of technical debt.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intuit found that GitHub&amp;rsquo;s SLA of 20 minutes meant they couldn&amp;rsquo;t rely on GitOps for region evacuation, so they wrote a &lt;code&gt;kubectl&lt;/code&gt; cron job that bypasses Git for failover. Pragmatic.&lt;/li&gt;
&lt;li&gt;Massive Jenkins ecosystems don&amp;rsquo;t get rewritten overnight. Intuit&amp;rsquo;s compromise: the last step in the Jenkins pipeline calls &lt;code&gt;argocd app sync&lt;/code&gt;. It&amp;rsquo;s push-based, yes, but the source of truth stays in Git. The Jenkins pipeline doesn&amp;rsquo;t own the desired state; it just tells ArgoCD &amp;ldquo;go reconcile now&amp;rdquo; instead of waiting for the next poll. It&amp;rsquo;s a stepping stone while teams migrate off Jenkins, not a permanent architecture.&lt;/li&gt;
&lt;li&gt;Continuous reconciliation with self-heal (auto-reverting manual changes) is where most teams deviate. In practice, many teams start with automated drift detection and alerting (so you know when someone runs a manual &lt;code&gt;kubectl&lt;/code&gt; command) but leave auto-remediation off until they&amp;rsquo;ve built confidence in their exclusion lists. That&amp;rsquo;s a reasonable middle ground.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pro Tip: Document your intentional deviations. Create an ADR (Architecture Decision Record) for each case where you&amp;rsquo;ve chosen not to follow pure GitOps, explaining why and what the plan is to close the gap (if ever). This turns exceptions into conscious decisions rather than accidental shortcuts.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;GitOps isn&amp;rsquo;t a silver bullet, but it&amp;rsquo;s one of the best approaches we have for managing Kubernetes at scale. The practices in this list aren&amp;rsquo;t theoretical. They&amp;rsquo;re what I&amp;rsquo;ve learned from running GitOps in production, watching talks from teams at Intuit and Statsig, and making every mistake in the book at least once.&lt;/p&gt;
&lt;p&gt;If I had to boil it all down to one sentence: &lt;strong&gt;treat your GitOps repo like production code, because it is.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Start with the basics (Git as source of truth, declarative config, pull-based delivery) and layer on the advanced practices as your team matures. Don&amp;rsquo;t try to implement all 12 on Day 1. Pick the three that address your biggest pain points and iterate from there.&lt;/p&gt;
&lt;p&gt;And remember, even the largest GitOps adopters in the world aren&amp;rsquo;t doing it perfectly. They&amp;rsquo;re being pragmatic. You should be too.&lt;/p&gt;
&lt;h3 id="how-pulumi-fits-into-your-gitops-workflow"&gt;How Pulumi fits into your GitOps workflow&lt;/h3&gt;
&lt;p&gt;Several of the practices in this post touch on problems Pulumi is built to solve. If you&amp;rsquo;re bridging IaC and GitOps, managing environment-specific configuration, or enforcing policy across both layers, here&amp;rsquo;s where to start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pulumi.com/docs/get-started/"&gt;Pulumi IaC&lt;/a&gt;&lt;/strong&gt;: Define cloud infrastructure in TypeScript, Python, or Go and use stack outputs to feed metadata into your GitOps manifests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pulumi.com/docs/esc/"&gt;Pulumi ESC&lt;/a&gt;&lt;/strong&gt;: Manage secrets and configuration across environments with fine-grained access controls, integrated with Kubernetes via the &lt;a href="https://www.pulumi.com/docs/esc/integrations/kubernetes/external-secrets-operator/"&gt;External Secrets Operator&lt;/a&gt; or the &lt;a href="https://www.pulumi.com/docs/esc/integrations/kubernetes/secret-store-csi-driver/"&gt;Secret Store CSI Driver&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pulumi.com/docs/iac/guides/continuous-delivery/pulumi-kubernetes-operator/"&gt;Pulumi Kubernetes Operator&lt;/a&gt;&lt;/strong&gt;: Let ArgoCD reconcile Pulumi stacks via GitOps, closing the loop between your IaC and Day 2 workflows.&lt;/li&gt;
&lt;/ul&gt;</description><author>Engin Diri</author><category>gitops</category><category>kubernetes</category><category>best-practices</category><category>argocd</category><category>devops</category></item><item><title>Kubernetes Best Practices I Wish I Had Known Before</title><link>https://www.pulumi.com/blog/kubernetes-best-practices-i-wish-i-had-known-before/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/kubernetes-best-practices-i-wish-i-had-known-before/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/kubernetes-best-practices-i-wish-i-had-known-before/index.png" /&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes best practices&lt;/strong&gt; are the production-tested rules that keep clusters reliable, secure, and affordable: set resource requests and limits on every container, isolate workloads with namespaces and NetworkPolicies, enforce least-privilege RBAC, automate health checks, ship via GitOps, validate every change with policy-as-code, generate an SBOM for every image, and manage Kubernetes itself with infrastructure as code instead of hand-rolled YAML. The 20 practices below cover what production teams actually do in 2026 — not what tutorials suggest.&lt;/p&gt;
&lt;h2 id="tldr-20-kubernetes-best-practices-for-2026"&gt;TL;DR: 20 Kubernetes best practices for 2026&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Set resource &lt;strong&gt;requests and limits&lt;/strong&gt; on every container.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;namespaces&lt;/strong&gt; plus &lt;strong&gt;ResourceQuota&lt;/strong&gt; and &lt;strong&gt;LimitRange&lt;/strong&gt; for isolation.&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;one container per Pod&lt;/strong&gt; unless you genuinely need a sidecar.&lt;/li&gt;
&lt;li&gt;Manage manifests with &lt;strong&gt;Helm, Kustomize, or Pulumi&lt;/strong&gt; — never raw copies.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Gateway API&lt;/strong&gt; (ingress-nginx is retired) for north-south traffic.&lt;/li&gt;
&lt;li&gt;Configure &lt;strong&gt;liveness, readiness, and startup probes&lt;/strong&gt; for every workload.&lt;/li&gt;
&lt;li&gt;Enforce &lt;strong&gt;RBAC&lt;/strong&gt; with the principle of least privilege from day one.&lt;/li&gt;
&lt;li&gt;Apply &lt;strong&gt;Pod Security Admission&lt;/strong&gt; at the &lt;code&gt;restricted&lt;/code&gt; level by default.&lt;/li&gt;
&lt;li&gt;Lock down east-west traffic with &lt;strong&gt;NetworkPolicy&lt;/strong&gt; (default-deny).&lt;/li&gt;
&lt;li&gt;Store secrets in an &lt;strong&gt;external secret manager&lt;/strong&gt; (Pulumi ESC, Vault, AWS Secrets Manager) — not as plain Kubernetes Secrets.&lt;/li&gt;
&lt;li&gt;Autoscale with &lt;strong&gt;HPA, VPA, and Cluster Autoscaler / Karpenter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Monitor with &lt;strong&gt;Prometheus, Grafana, and OpenTelemetry&lt;/strong&gt;; alert before users do.&lt;/li&gt;
&lt;li&gt;Deploy with &lt;strong&gt;GitOps&lt;/strong&gt; (Argo CD or Flux) — Git is the only source of truth.&lt;/li&gt;
&lt;li&gt;Gate every change with &lt;strong&gt;policy-as-code&lt;/strong&gt; (Pulumi CrossGuard, OPA/Gatekeeper, Kyverno).&lt;/li&gt;
&lt;li&gt;Generate and verify an &lt;strong&gt;SBOM&lt;/strong&gt; for every image; sign with &lt;strong&gt;Sigstore/cosign&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Manage cost with &lt;strong&gt;FinOps&lt;/strong&gt; practices: right-size, schedule, and chargeback by namespace.&lt;/li&gt;
&lt;li&gt;Keep clusters and add-ons &lt;strong&gt;patched&lt;/strong&gt; on a documented cadence.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;labels and annotations&lt;/strong&gt; consistently for ownership, cost, and SLO tracking.&lt;/li&gt;
&lt;li&gt;Separate &lt;strong&gt;dev, staging, and prod&lt;/strong&gt; clusters — or virtualize with vCluster.&lt;/li&gt;
&lt;li&gt;Treat clusters as &lt;strong&gt;cattle, not pets&lt;/strong&gt;: rebuild from code, never edit live.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Last updated April 30, 2026&lt;/strong&gt; — added GitOps, policy-as-code, SBOM/supply-chain, autoscaling, NetworkPolicy, and FinOps sections; added an anti-pattern table; restructured headings around the questions teams actually ask.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;&lt;p&gt;Going to &lt;a href="https://www.pulumi.com/kubecon/"&gt;KubeCon Europe 2026&lt;/a&gt;? Tame Kubernetes complexity with code.&lt;/p&gt;
&lt;p&gt;Pulumi demos, platform engineering talks, AI-powered Neo in action, and yes, free plushies. &lt;a href="https://www.pulumi.com/kubecon/"&gt;Booth 784&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure&gt;&lt;img src="https://www.pulumi.com/blog/kubernetes-best-practices-i-wish-i-had-known-before/img.png"
alt="The &amp;#39;Kubernetes is easy&amp;#39;-iceberg meme is a classic example of how Kubernetes can be deceivingly complex" width="100%"&gt;&lt;figcaption&gt;
&lt;p&gt;The &amp;lsquo;Kubernetes is easy&amp;rsquo;-iceberg meme is a classic example of how Kubernetes can be deceivingly complex&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="what-are-kubernetes-best-practices"&gt;What are Kubernetes best practices?&lt;/h2&gt;
&lt;p&gt;Kubernetes best practices are the operational patterns that keep workloads available, secure, observable, and cost-efficient on a Kubernetes cluster. They span four areas: &lt;strong&gt;workload configuration&lt;/strong&gt; (resources, probes, images), &lt;strong&gt;security&lt;/strong&gt; (RBAC, NetworkPolicy, Pod Security, secrets, supply chain), &lt;strong&gt;delivery&lt;/strong&gt; (GitOps, policy-as-code, IaC), and &lt;strong&gt;platform operations&lt;/strong&gt; (autoscaling, monitoring, upgrades, FinOps). The rest of this post explains each one with a copy-pasteable rule, a definition of the underlying Kubernetes object, and a common anti-pattern to avoid.&lt;/p&gt;
&lt;h2 id="common-kubernetes-anti-patterns-vs-the-correct-approach"&gt;Common Kubernetes anti-patterns vs. the correct approach&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common mistake&lt;/th&gt;
&lt;th&gt;What goes wrong&lt;/th&gt;
&lt;th&gt;Correct approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No resource requests or limits&lt;/td&gt;
&lt;td&gt;Noisy-neighbor evictions, OOMKills, unschedulable Pods&lt;/td&gt;
&lt;td&gt;Set requests for &lt;strong&gt;every&lt;/strong&gt; container; use VPA recommendations to tune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything in the &lt;code&gt;default&lt;/code&gt; namespace&lt;/td&gt;
&lt;td&gt;No isolation, no quota, no RBAC boundary&lt;/td&gt;
&lt;td&gt;One namespace per team or environment + ResourceQuota + RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster-admin&lt;/code&gt; for service accounts&lt;/td&gt;
&lt;td&gt;Full-cluster blast radius on compromise&lt;/td&gt;
&lt;td&gt;Per-namespace Role + RoleBinding (least privilege)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain Kubernetes &lt;code&gt;Secret&lt;/code&gt; objects&lt;/td&gt;
&lt;td&gt;Base64 ≠ encryption; secrets leak via etcd, kubectl, audit logs&lt;/td&gt;
&lt;td&gt;External Secrets Operator + &lt;a href="https://www.pulumi.com/docs/esc/"&gt;Pulumi ESC&lt;/a&gt;, Vault, or AWS Secrets Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open east-west traffic&lt;/td&gt;
&lt;td&gt;One compromised Pod can reach every service&lt;/td&gt;
&lt;td&gt;Default-deny NetworkPolicy; allow only required flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubectl apply -f&lt;/code&gt; from a laptop&lt;/td&gt;
&lt;td&gt;No history, no review, no rollback&lt;/td&gt;
&lt;td&gt;GitOps with Argo CD or Flux; Git is the source of truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual &lt;code&gt;latest&lt;/code&gt; image tags&lt;/td&gt;
&lt;td&gt;Non-reproducible deploys, supply-chain risk&lt;/td&gt;
&lt;td&gt;Pinned digests (&lt;code&gt;@sha256:...&lt;/code&gt;) with cosign verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingress-nginx in 2026&lt;/td&gt;
&lt;td&gt;Project retired March 2026 — no security fixes&lt;/td&gt;
&lt;td&gt;Migrate to the Gateway API (Envoy Gateway, Istio, Kgateway)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolled YAML in dozens of repos&lt;/td&gt;
&lt;td&gt;Drift, copy-paste bugs, no testing&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pulumi.com/docs/iac/get-started/kubernetes/"&gt;Pulumi&lt;/a&gt;, Helm, or Kustomize with reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One shared &lt;code&gt;prod&lt;/code&gt; cluster for everything&lt;/td&gt;
&lt;td&gt;Blast radius spans every team&lt;/td&gt;
&lt;td&gt;Separate clusters or &lt;a href="https://www.vcluster.com/"&gt;vCluster&lt;/a&gt; virtual clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="1-how-do-you-set-kubernetes-resource-requests-and-limits"&gt;1. How do you set Kubernetes resource requests and limits?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What are resource requests and limits?&lt;/strong&gt; A resource &lt;em&gt;request&lt;/em&gt; is the minimum CPU and memory the kube-scheduler guarantees a container; a resource &lt;em&gt;limit&lt;/em&gt; is the maximum the kubelet allows it to consume before throttling CPU or OOMKilling memory. Pods without requests are scheduled blind, so a noisy neighbor can evict your workload at any time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; every container in production sets a request for both CPU and memory, and a memory limit. CPU limits are optional and frequently counter-productive — they cause throttling spikes that look like outages.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start at &lt;strong&gt;100–200m CPU and 128–512Mi memory&lt;/strong&gt; for a typical web service.&lt;/li&gt;
&lt;li&gt;Tune from real data using &lt;strong&gt;Vertical Pod Autoscaler&lt;/strong&gt; with &lt;code&gt;updateMode: Off&lt;/code&gt; (recommendation-only) or Prometheus histograms.&lt;/li&gt;
&lt;li&gt;Use a per-namespace &lt;code&gt;LimitRange&lt;/code&gt; so teams cannot ship a container with no requests at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-how-should-you-structure-kubernetes-namespaces"&gt;2. How should you structure Kubernetes namespaces?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is a Kubernetes namespace?&lt;/strong&gt; A namespace is a logical partition of cluster resources. It scopes object names, RBAC bindings, NetworkPolicies, and quotas — but does not provide hard isolation by itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; never deploy production workloads to &lt;code&gt;default&lt;/code&gt;. Map namespaces to ownership boundaries — usually one per team, environment, or tenant — and pair every namespace with three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;ResourceQuota&lt;/code&gt;&lt;/strong&gt; to cap aggregate CPU, memory, and object counts.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;LimitRange&lt;/code&gt;&lt;/strong&gt; to enforce per-container defaults.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;RoleBinding&lt;/code&gt;&lt;/strong&gt; that grants only the permissions that team needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the smallest unit that gives you predictable cost, blast-radius containment, and an RBAC perimeter.&lt;/p&gt;
&lt;h2 id="3-should-you-run-multiple-containers-in-one-pod"&gt;3. Should you run multiple containers in one Pod?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is a Pod?&lt;/strong&gt; A Pod is the smallest deployable unit in Kubernetes — one or more containers that share a network namespace, IPC, and storage volumes, and that are scheduled and scaled as a single unit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; one container per Pod by default. Add a second container only when it must share the Pod&amp;rsquo;s network or volumes — for example, a service-mesh sidecar (Istio/Linkerd), a logging shipper, or an &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"&gt;init container&lt;/a&gt; that prepares state before the main process starts. Multi-container Pods couple lifecycles and scaling, so reach for them deliberately, not by default.&lt;/p&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/master-kubernetes-secrets-with-pulumi-esc-secrets-store-csi-driver/"&gt;
Master Kubernetes Secrets with Pulumi ESC &amp;#43; Secrets Store CSI Driver
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/aws-eks-auto-mode/"&gt;
Getting Started with Amazon EKS Auto Mode in Pulumi
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/why-every-platform-engineer-should-care-about-kubernetes-operators/"&gt;
Why Every Platform Engineer Should Care About Kubernetes Operators
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="4-should-you-use-helm-kustomize-or-pulumi-for-kubernetes-manifests"&gt;4. Should you use Helm, Kustomize, or Pulumi for Kubernetes manifests?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; never copy-paste raw YAML across services. Pick one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://helm.sh/"&gt;Helm&lt;/a&gt;&lt;/strong&gt; — Go-templated charts; the de-facto distribution format for off-the-shelf software (Prometheus, cert-manager, Argo CD).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://kustomize.io/"&gt;Kustomize&lt;/a&gt;&lt;/strong&gt; — overlay-based, no templating; ships in &lt;code&gt;kubectl&lt;/code&gt;. Best for &amp;ldquo;same app, different environment&amp;rdquo; diffs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pulumi.com/docs/iac/get-started/kubernetes/"&gt;Pulumi&lt;/a&gt;&lt;/strong&gt; — real programming languages (TypeScript, Python, Go, Java, .NET) for Kubernetes plus the cloud resources around it (EKS, GKE, AKS, DNS, IAM). Type checking, tests, and the Pulumi &lt;a href="https://www.pulumi.com/docs/integrations/clouds/kubernetes/pulumi-kubernetes-operator/"&gt;Kubernetes Operator&lt;/a&gt; for GitOps-style reconciliation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are already managing cloud infrastructure with code, extending the same Pulumi program to Kubernetes is the smallest cognitive jump — the cluster, its IAM, its DNS, and its workloads live in one stack and one review.&lt;/p&gt;
&lt;h2 id="5-how-should-you-handle-ingress-and-networking-on-kubernetes-in-2026"&gt;5. How should you handle ingress and networking on Kubernetes in 2026?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the Gateway API?&lt;/strong&gt; The &lt;a href="https://gateway-api.sigs.k8s.io/"&gt;Gateway API&lt;/a&gt; is the successor to the legacy &lt;code&gt;Ingress&lt;/code&gt; resource. It splits responsibility between infrastructure (&lt;code&gt;GatewayClass&lt;/code&gt;, &lt;code&gt;Gateway&lt;/code&gt;) and application (&lt;code&gt;HTTPRoute&lt;/code&gt;, &lt;code&gt;GRPCRoute&lt;/code&gt;, &lt;code&gt;TLSRoute&lt;/code&gt;) teams, and it is the SIG-Network direction for north-south traffic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; new clusters should standardize on the Gateway API. The widely deployed &lt;code&gt;ingress-nginx&lt;/code&gt; controller &lt;a href="https://www.pulumi.com/blog/ingress-nginx-to-gateway-api-kgateway/"&gt;retired in March 2026&lt;/a&gt;, and remaining deployments should migrate to Envoy Gateway, Istio, Linkerd Gateway, or Kgateway.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terminate TLS at the gateway and automate certificates with &lt;a href="https://cert-manager.io/"&gt;cert-manager&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;path-based and host-based routing&lt;/strong&gt; instead of one gateway per service.&lt;/li&gt;
&lt;li&gt;Protect every gateway with a WAF (managed cloud WAF or &lt;a href="https://coraza.io/"&gt;Coraza&lt;/a&gt; on Envoy).&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; align-items: center; justify-content: center; height: 100%;"&gt;
&lt;img src="img_1.png" alt="A meme featuring two side-by-side images of Mr. Incredible. The left side shows a normal, happy face labeled “I’M LEARNING KUBERNETES”, while the right side is a dark, distorted version of his face with the same caption, humorously implying the increasing complexity and challenges of learning Kubernetes." style="width: 80%; height: 100%"&gt;
&lt;/div&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;&lt;p&gt;ingress-nginx is retiring in March 2026.&lt;/p&gt;
&lt;p&gt;Here is a guide on &lt;a href="https://www.pulumi.com/blog/ingress-nginx-to-gateway-api-kgateway/"&gt;How to Move to the Gateway API: post ingress-nginx Retirement&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="6-whats-the-difference-between-liveness-readiness-and-startup-probes"&gt;6. What&amp;rsquo;s the difference between liveness, readiness, and startup probes?&lt;/h2&gt;
&lt;p&gt;Probes tell the kubelet whether to restart a container, route traffic to it, or wait for it. Configure them per workload:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Liveness probe&lt;/strong&gt; — restarts a hung container. Use sparingly; a bad liveness probe is a self-inflicted DoS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readiness probe&lt;/strong&gt; — gates traffic from Services and Gateway routes. Use for &lt;em&gt;every&lt;/em&gt; network-facing workload.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Startup probe&lt;/strong&gt; — gives slow-booting apps (JVM, ML models) time to come up before liveness checks begin. Use whenever cold-start exceeds 10 seconds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/"&gt;the upstream guide&lt;/a&gt; for HTTP, TCP, and exec probe syntax. Tune &lt;code&gt;failureThreshold&lt;/code&gt;, &lt;code&gt;periodSeconds&lt;/code&gt;, and &lt;code&gt;timeoutSeconds&lt;/code&gt; based on real latency, not defaults.&lt;/p&gt;
&lt;h2 id="7-how-do-you-secure-a-kubernetes-cluster"&gt;7. How do you secure a Kubernetes cluster?&lt;/h2&gt;
&lt;p&gt;Cluster security is layered: identity, workload posture, network, and secrets. Get all four right.&lt;/p&gt;
&lt;h3 id="what-is-rbac-in-kubernetes"&gt;What is RBAC in Kubernetes?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;RBAC (Role-Based Access Control)&lt;/strong&gt; is the Kubernetes authorization model that maps subjects (users, groups, ServiceAccounts) to verbs (&lt;code&gt;get&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;) on resources, scoped to a namespace (&lt;code&gt;Role&lt;/code&gt;) or the whole cluster (&lt;code&gt;ClusterRole&lt;/code&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Grant &lt;strong&gt;least privilege&lt;/strong&gt; — never bind humans or workloads to &lt;code&gt;cluster-admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prefer per-namespace &lt;code&gt;Role&lt;/code&gt; + &lt;code&gt;RoleBinding&lt;/code&gt; over &lt;code&gt;ClusterRoleBinding&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Audit with &lt;code&gt;kubectl auth can-i --as=...&lt;/code&gt; and tools like &lt;a href="https://github.com/FairwindsOps/rbac-lookup"&gt;&lt;code&gt;rbac-lookup&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-is-pod-security-admission"&gt;What is Pod Security Admission?&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/"&gt;Pod Security Admission&lt;/a&gt; is the built-in admission controller that enforces the three Pod Security Standards (&lt;code&gt;privileged&lt;/code&gt;, &lt;code&gt;baseline&lt;/code&gt;, &lt;code&gt;restricted&lt;/code&gt;) at the namespace level.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Label every namespace &lt;code&gt;pod-security.kubernetes.io/enforce=restricted&lt;/code&gt; by default.&lt;/li&gt;
&lt;li&gt;Drop &lt;code&gt;NET_RAW&lt;/code&gt; and all capabilities; run as non-root with a read-only root filesystem.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;seccompProfile: RuntimeDefault&lt;/code&gt; and a tight &lt;code&gt;securityContext&lt;/code&gt; for every container.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-is-a-networkpolicy"&gt;What is a NetworkPolicy?&lt;/h3&gt;
&lt;p&gt;A &lt;strong&gt;NetworkPolicy&lt;/strong&gt; is a Kubernetes object that defines allowed ingress and egress traffic for selected Pods at the IP/port level. Without one, every Pod can reach every other Pod by default.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apply a &lt;strong&gt;default-deny&lt;/strong&gt; ingress and egress policy in every namespace.&lt;/li&gt;
&lt;li&gt;Allow only the flows your service actually needs (DNS, the database, the upstream API).&lt;/li&gt;
&lt;li&gt;For richer L7 controls (mTLS, JWT auth), layer a service mesh on top.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-should-you-manage-kubernetes-secrets"&gt;How should you manage Kubernetes secrets?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Built-in &lt;code&gt;Secret&lt;/code&gt; objects are base64-encoded, not encrypted.&lt;/strong&gt; Treat them as a transport, not as storage.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store the source of truth in &lt;a href="https://www.pulumi.com/docs/esc/"&gt;Pulumi ESC&lt;/a&gt;, &lt;a href="https://www.vaultproject.io/"&gt;HashiCorp Vault&lt;/a&gt;, &lt;a href="https://aws.amazon.com/secrets-manager/"&gt;AWS Secrets Manager&lt;/a&gt;, GCP Secret Manager, or Azure Key Vault.&lt;/li&gt;
&lt;li&gt;Project secrets into Pods with the &lt;a href="https://www.pulumi.com/docs/esc/integrations/kubernetes/external-secrets-operator/"&gt;External Secrets Operator&lt;/a&gt; or the &lt;a href="https://www.pulumi.com/docs/esc/integrations/kubernetes/secret-store-csi-driver/"&gt;Secret Store CSI Driver&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;etcd encryption-at-rest&lt;/strong&gt; with a KMS provider so leaked etcd snapshots stay opaque.&lt;/li&gt;
&lt;li&gt;Rotate automatically; never commit secrets to Git, even encrypted.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="8-how-do-you-set-up-kubernetes-observability"&gt;8. How do you set up Kubernetes observability?&lt;/h2&gt;
&lt;p&gt;Observability rests on the three pillars: metrics, logs, and traces. The 2026 default stack is OpenTelemetry-first.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; — &lt;a href="https://github.com/prometheus-operator/kube-prometheus"&gt;Prometheus&lt;/a&gt; (or a Prometheus-compatible TSDB like Mimir, Thanos, VictoriaMetrics) plus Grafana dashboards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs&lt;/strong&gt; — Fluent Bit or the OpenTelemetry Collector shipping structured JSON to Loki, Elastic/OpenSearch, or a managed service. Never depend on &lt;code&gt;kubectl logs&lt;/code&gt; for incidents — Pods are ephemeral.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traces&lt;/strong&gt; — OpenTelemetry SDKs in your apps emitting to Tempo, Jaeger, or a vendor backend. Correlate trace IDs with logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alerts&lt;/strong&gt; — page on &lt;strong&gt;SLO burn rate&lt;/strong&gt;, not on every crashed Pod. Burn-rate alerting catches real user impact without firing on every restart.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="9-how-do-you-automate-kubernetes-deployments-with-gitops"&gt;9. How do you automate Kubernetes deployments with GitOps?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is GitOps?&lt;/strong&gt; &lt;a href="https://opengitops.dev/"&gt;GitOps&lt;/a&gt; is a deployment model where the desired cluster state lives in a Git repository and a controller continuously reconciles the live cluster to match. Changes happen through pull requests, not &lt;code&gt;kubectl apply&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; every cluster has a controller — &lt;a href="https://argoproj.github.io/cd/"&gt;Argo CD&lt;/a&gt;, &lt;a href="https://fluxcd.io/"&gt;Flux&lt;/a&gt;, or the &lt;a href="https://www.pulumi.com/docs/integrations/clouds/kubernetes/pulumi-kubernetes-operator/"&gt;Pulumi Kubernetes Operator&lt;/a&gt; — that owns reconciliation. Humans never &lt;code&gt;kubectl apply&lt;/code&gt; to production.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;app-of-apps&lt;/strong&gt; (Argo) or &lt;strong&gt;Kustomization trees&lt;/strong&gt; (Flux) to bootstrap whole clusters from a single root.&lt;/li&gt;
&lt;li&gt;Keep manifests, Helm values, and Pulumi stack references in Git, with environment promotion via PR.&lt;/li&gt;
&lt;li&gt;Combine GitOps with &lt;a href="https://www.pulumi.com/docs/deployments/deployments/"&gt;Pulumi Deployments&lt;/a&gt; so cloud infrastructure (VPCs, EKS clusters, IAM, DNS) and the workloads on top promote through the same review pipeline. See &lt;a href="https://www.pulumi.com/blog/improving-gitops-with-pulumi-operator/"&gt;improving GitOps with the Pulumi Operator&lt;/a&gt; for a worked example.&lt;/li&gt;
&lt;li&gt;Configure &lt;strong&gt;automated rollback&lt;/strong&gt; on health-check failure, and &lt;strong&gt;drift detection&lt;/strong&gt; alerts when someone edits live state.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="10-how-often-should-you-upgrade-kubernetes"&gt;10. How often should you upgrade Kubernetes?&lt;/h2&gt;
&lt;p&gt;Kubernetes ships a minor release roughly &lt;a href="https://kubernetes.io/releases/release/"&gt;every four months&lt;/a&gt; and supports each one for about 14 months. Treat upgrades as routine maintenance, not a project.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stay within &lt;strong&gt;two minor versions&lt;/strong&gt; of upstream — older versions miss CVE patches.&lt;/li&gt;
&lt;li&gt;Test upgrades in a non-prod cluster, run a &lt;a href="https://github.com/cncf/k8s-conformance"&gt;conformance test&lt;/a&gt;, then promote.&lt;/li&gt;
&lt;li&gt;Back up etcd before control-plane changes; managed services (EKS, GKE, AKS) handle most of this for you.&lt;/li&gt;
&lt;li&gt;Upgrade add-ons (CNI, CSI, Gateway controller, cert-manager, metrics-server) on the same cadence — pin versions in code so the upgrade is auditable.&lt;/li&gt;
&lt;li&gt;Watch the &lt;a href="https://kubernetes.io/docs/reference/using-api/deprecation-guide/"&gt;Kubernetes deprecation guide&lt;/a&gt; and run &lt;a href="https://github.com/FairwindsOps/pluto"&gt;&lt;code&gt;pluto&lt;/code&gt;&lt;/a&gt; or &lt;code&gt;kubent&lt;/code&gt; to find deprecated APIs before they break.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="11-how-should-you-label-and-annotate-kubernetes-resources"&gt;11. How should you label and annotate Kubernetes resources?&lt;/h2&gt;
&lt;p&gt;Labels and annotations are the metadata layer that everything — Services, NetworkPolicies, monitoring, cost tools, GitOps — keys off of. Consistency matters more than cleverness.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adopt the &lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/"&gt;recommended labels&lt;/a&gt;: &lt;code&gt;app.kubernetes.io/name&lt;/code&gt;, &lt;code&gt;app.kubernetes.io/instance&lt;/code&gt;, &lt;code&gt;app.kubernetes.io/version&lt;/code&gt;, &lt;code&gt;app.kubernetes.io/component&lt;/code&gt;, &lt;code&gt;app.kubernetes.io/part-of&lt;/code&gt;, &lt;code&gt;app.kubernetes.io/managed-by&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add organization-specific labels for &lt;strong&gt;owner&lt;/strong&gt;, &lt;strong&gt;cost-center&lt;/strong&gt;, &lt;strong&gt;environment&lt;/strong&gt;, and &lt;strong&gt;SLO tier&lt;/strong&gt; — your FinOps and on-call processes will need them.&lt;/li&gt;
&lt;li&gt;Reserve &lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"&gt;annotations&lt;/a&gt; for non-identifying metadata: build SHA, change-ticket, last-deployed timestamp, ingress controller hints.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="rounded-lg bg-violet-50 p-6 my-8"&gt;
&lt;p class="heading-4 m-0 mb-3 flex items-center gap-1.5"&gt;Manage Kubernetes with code&lt;/p&gt;
&lt;div class="body-base m-0 text-gray-950"&gt;Provision your clusters, their cloud resources, and the workloads on them from one Pulumi program, and use stacks to promote changes from dev to production.&lt;/div&gt;
&lt;a href="https://app.pulumi.com/signup" data-track="blog-body-cta" class="btn btn-primary mt-4"&gt;
Get started
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--regular size-4" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-arrow-right-regular"/&gt;&lt;/svg&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id="12-how-should-you-separate-kubernetes-environments"&gt;12. How should you separate Kubernetes environments?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; production is its own cluster. Dev and staging can share — sometimes. Mixing prod with anything else couples blast radius, upgrade cadence, and quotas.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Separate clusters per environment&lt;/strong&gt; is the safe default. With managed control planes (EKS, GKE, AKS) the cost is small.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual clusters&lt;/strong&gt; with &lt;a href="https://www.vcluster.com/"&gt;vCluster&lt;/a&gt; give each team a Kubernetes API of their own without the operational cost of a real cluster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Namespace-only segregation&lt;/strong&gt; can work for dev/staging if you enforce strict NetworkPolicy, RBAC, and ResourceQuota boundaries.&lt;/li&gt;
&lt;li&gt;Provision all of them from the same &lt;a href="https://www.pulumi.com/docs/iac/get-started/kubernetes/"&gt;Pulumi&lt;/a&gt; program with different stacks — &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt; — so promotion is a config change, not a copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="13-how-do-you-optimize-kubernetes-container-images"&gt;13. How do you optimize Kubernetes container images?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Start from a &lt;strong&gt;distroless&lt;/strong&gt; (&lt;a href="https://github.com/GoogleContainerTools/distroless"&gt;gcr.io/distroless&lt;/a&gt;) or &lt;code&gt;chainguard/static&lt;/code&gt; base. Alpine is fine if you accept musl.&lt;/li&gt;
&lt;li&gt;Build with &lt;strong&gt;multi-stage Dockerfiles&lt;/strong&gt; so build tools never ship in the final image.&lt;/li&gt;
&lt;li&gt;Pin the base image &lt;strong&gt;by digest&lt;/strong&gt; (&lt;code&gt;@sha256:...&lt;/code&gt;), not by tag — &lt;code&gt;latest&lt;/code&gt; and &lt;code&gt;1.21&lt;/code&gt; move under you.&lt;/li&gt;
&lt;li&gt;Scan every image with &lt;strong&gt;&lt;a href="https://trivy.dev/latest/"&gt;Trivy&lt;/a&gt;&lt;/strong&gt;, &lt;a href="https://github.com/anchore/grype"&gt;Grype&lt;/a&gt;, or your registry&amp;rsquo;s built-in scanner. Block builds on &lt;code&gt;Critical&lt;/code&gt;/&lt;code&gt;High&lt;/code&gt; CVEs.&lt;/li&gt;
&lt;li&gt;Smaller images mean faster pulls, faster autoscaling, and a smaller attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="14-whats-a-reliable-kubernetes-logging-strategy"&gt;14. What&amp;rsquo;s a reliable Kubernetes logging strategy?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralize&lt;/strong&gt; to Loki, Elastic/OpenSearch, or a managed service via Fluent Bit or the OpenTelemetry Collector running as a DaemonSet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structure&lt;/strong&gt; logs as JSON with consistent field names (&lt;code&gt;trace_id&lt;/code&gt;, &lt;code&gt;span_id&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retain&lt;/strong&gt; by tier — 7 days hot, 30–90 days warm, archive to object storage for compliance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redact&lt;/strong&gt; secrets and PII at the collector, not after the fact.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/master-kubernetes-secrets-with-pulumi-esc-secrets-store-csi-driver/"&gt;
Master Kubernetes Secrets with Pulumi ESC &amp;#43; Secrets Store CSI Driver
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/aws-eks-auto-mode/"&gt;
Getting Started with Amazon EKS Auto Mode in Pulumi
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/why-every-platform-engineer-should-care-about-kubernetes-operators/"&gt;
Why Every Platform Engineer Should Care About Kubernetes Operators
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="15-how-should-you-autoscale-workloads-on-kubernetes"&gt;15. How should you autoscale workloads on Kubernetes?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the Horizontal Pod Autoscaler?&lt;/strong&gt; The &lt;strong&gt;HPA&lt;/strong&gt; scales the replica count of a Deployment or StatefulSet up and down based on CPU, memory, or custom/external metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the Vertical Pod Autoscaler?&lt;/strong&gt; The &lt;strong&gt;VPA&lt;/strong&gt; adjusts a Pod&amp;rsquo;s CPU and memory &lt;em&gt;requests&lt;/em&gt; over time based on observed usage. Run it with &lt;code&gt;updateMode: Off&lt;/code&gt; (recommendation-only) in production and apply the recommendations through your IaC tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are Cluster Autoscaler and Karpenter?&lt;/strong&gt; &lt;strong&gt;Cluster Autoscaler&lt;/strong&gt; adds and removes nodes to match Pod demand. &lt;strong&gt;&lt;a href="https://karpenter.sh/"&gt;Karpenter&lt;/a&gt;&lt;/strong&gt; is a faster, group-less node provisioner now widely used on EKS and gaining traction on other clouds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; every production workload has an HPA. Every cluster has a node-level autoscaler. Set sensible &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; replicas, define a &lt;code&gt;PodDisruptionBudget&lt;/code&gt;, and gate scale-down behavior so traffic spikes don&amp;rsquo;t pile up at restart time. Pair with &lt;strong&gt;KEDA&lt;/strong&gt; for event-driven scaling (queues, Kafka lag, scheduled scale).&lt;/p&gt;
&lt;h2 id="16-how-do-you-enforce-policy-as-code-on-kubernetes"&gt;16. How do you enforce policy-as-code on Kubernetes?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is policy-as-code?&lt;/strong&gt; Policy-as-code expresses governance rules — security baselines, tagging, region restrictions, cost guardrails — as code that runs in CI and at admission time, blocking non-compliant changes before they reach the cluster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; every change goes through at least one policy engine.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pre-deploy&lt;/strong&gt; — validate Pulumi or Helm output with &lt;a href="https://www.pulumi.com/docs/insights/policy/"&gt;Pulumi CrossGuard&lt;/a&gt; or &lt;a href="https://www.conftest.dev/"&gt;Conftest&lt;/a&gt; in CI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admission-time&lt;/strong&gt; — install &lt;a href="https://open-policy-agent.github.io/gatekeeper/"&gt;OPA Gatekeeper&lt;/a&gt; or &lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; and require, for example, signed images, a &lt;code&gt;team&lt;/code&gt; label, and a non-root &lt;code&gt;securityContext&lt;/code&gt; on every Pod.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous&lt;/strong&gt; — scan running clusters with &lt;a href="https://www.pulumi.com/docs/insights/policy/"&gt;Pulumi Insights&lt;/a&gt; or &lt;code&gt;kubescape&lt;/code&gt; for drift from policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a href="https://www.pulumi.com/blog/benefits-of-policy-as-code/"&gt;the benefits of policy-as-code&lt;/a&gt; and &lt;a href="https://www.pulumi.com/blog/enforcing-policy-as-code-on-discovered-resources-with-pulumi/"&gt;enforcing policy-as-code on discovered resources&lt;/a&gt; for end-to-end examples.&lt;/p&gt;
&lt;h2 id="17-how-do-you-secure-the-kubernetes-software-supply-chain"&gt;17. How do you secure the Kubernetes software supply chain?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is an SBOM?&lt;/strong&gt; A &lt;strong&gt;Software Bill of Materials&lt;/strong&gt; is a machine-readable inventory of every component in a container image — packages, versions, licenses, hashes — typically in &lt;a href="https://spdx.dev/"&gt;SPDX&lt;/a&gt; or &lt;a href="https://cyclonedx.org/"&gt;CycloneDX&lt;/a&gt; format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; every image you run in production has a verified SBOM and a verified signature.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate SBOMs at build time with &lt;a href="https://github.com/anchore/syft"&gt;Syft&lt;/a&gt; or &lt;code&gt;docker sbom&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sign images and SBOMs with &lt;a href="https://www.sigstore.dev/"&gt;Sigstore/cosign&lt;/a&gt; keylessly via GitHub OIDC.&lt;/li&gt;
&lt;li&gt;Verify signatures at admission with &lt;a href="https://kyverno.io/policies/?policytypes=Cosign"&gt;Kyverno&lt;/a&gt; or &lt;a href="https://github.com/sse-secure-systems/connaisseur"&gt;Connaisseur&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Generate &lt;strong&gt;provenance attestations&lt;/strong&gt; (&lt;a href="https://slsa.dev/"&gt;SLSA&lt;/a&gt;) so you can prove which build pipeline produced a given image.&lt;/li&gt;
&lt;li&gt;Pin base images by digest and rebuild on a cadence so CVE patches actually land.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what regulators and customers ask for in 2026 — Executive Order 14028, the EU Cyber Resilience Act, and most enterprise procurement checklists all require it.&lt;/p&gt;
&lt;h2 id="18-how-do-you-manage-kubernetes-cost-finops"&gt;18. How do you manage Kubernetes cost (FinOps)?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; Kubernetes cost is a labeling and right-sizing problem before it is a discount problem.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Right-size&lt;/strong&gt; with VPA recommendations, &lt;a href="https://github.com/FairwindsOps/goldilocks"&gt;Goldilocks&lt;/a&gt;, or your APM&amp;rsquo;s recommendation engine. Most workloads request more CPU than they actually use; right-sizing is usually the largest single saving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schedule&lt;/strong&gt; non-prod clusters off overnight and on weekends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spot/preemptible nodes&lt;/strong&gt; for fault-tolerant workloads; use Karpenter&amp;rsquo;s consolidation to compact bin-packing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Show back / charge back&lt;/strong&gt; by namespace and label using &lt;a href="https://www.opencost.io/"&gt;OpenCost&lt;/a&gt; or &lt;a href="https://www.kubecost.io/"&gt;Kubecost&lt;/a&gt;. Without per-team attribution, no team owns cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provision the underlying cluster with IaC&lt;/strong&gt; — see &lt;a href="https://www.pulumi.com/docs/iac/get-started/kubernetes/"&gt;Pulumi for Kubernetes&lt;/a&gt; — so node types, autoscaling limits, and reserved capacity are reviewable artifacts. Pulumi&amp;rsquo;s &lt;a href="https://www.pulumi.com/blog/hidden-costs-of-infrastructure-management/"&gt;hidden-cost analysis&lt;/a&gt; walks through where the money actually goes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="19-why-should-you-treat-kubernetes-clusters-as-cattle-not-pets"&gt;19. Why should you treat Kubernetes clusters as cattle, not pets?&lt;/h2&gt;
&lt;p&gt;The old &amp;ldquo;cattle, not pets&amp;rdquo; adage applies more strictly to Kubernetes than to VMs. Manual edits to a live cluster will be reconciled away, drift between environments, or vanish on the next Pod restart.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fix problems in code — YAML, Helm chart, or &lt;a href="https://www.pulumi.com/docs/iac/get-started/kubernetes/"&gt;Pulumi program&lt;/a&gt; — and let the controller redeploy.&lt;/li&gt;
&lt;li&gt;If you can&amp;rsquo;t redeploy a cluster from Git in under an hour, you have a pet.&lt;/li&gt;
&lt;li&gt;Use ephemeral preview environments for PRs; use blue/green or progressive delivery (Argo Rollouts, Flagger) for production cutovers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="20-why-use-pulumi-to-manage-kubernetes"&gt;20. Why use Pulumi to manage Kubernetes?&lt;/h2&gt;
&lt;p&gt;Native YAML scales until your team doesn&amp;rsquo;t. Once you have more than a handful of services, dependencies between cloud resources and Kubernetes objects, or more than one cluster, &lt;strong&gt;infrastructure as code in a real programming language&lt;/strong&gt; wins on every axis.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real languages&lt;/strong&gt; — TypeScript, Python, Go, Java, .NET — for &lt;a href="https://www.pulumi.com/docs/iac/languages-sdks/"&gt;type-safe, testable Kubernetes infrastructure&lt;/a&gt;. Loops, conditionals, and unit tests instead of templating.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One stack, full topology&lt;/strong&gt; — manage the cloud (EKS/GKE/AKS, VPC, IAM, DNS) and the workloads in it together. See &lt;a href="https://www.pulumi.com/blog/easily-create-and-manage-aws-eks-kubernetes-clusters-with-pulumi/"&gt;easily create and manage AWS EKS clusters with Pulumi&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusable components&lt;/strong&gt; — abstract platform patterns into &lt;a href="https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/"&gt;Pulumi packages&lt;/a&gt; other teams &lt;code&gt;import&lt;/code&gt; instead of copy-pasting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitOps reconciliation&lt;/strong&gt; — the &lt;a href="https://www.pulumi.com/docs/integrations/clouds/kubernetes/pulumi-kubernetes-operator/"&gt;Pulumi Kubernetes Operator&lt;/a&gt; reconciles a cluster to a Pulumi stack on every Git push.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy-as-code&lt;/strong&gt; built in — &lt;a href="https://www.pulumi.com/docs/insights/policy/"&gt;CrossGuard&lt;/a&gt; blocks non-compliant changes before &lt;code&gt;pulumi up&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets done right&lt;/strong&gt; — &lt;a href="https://www.pulumi.com/docs/esc/"&gt;Pulumi ESC&lt;/a&gt; federates secrets and configuration across stacks, environments, and Kubernetes clusters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a deeper comparison of hand-written YAML, Terraform, and Pulumi for Kubernetes, see &lt;a href="https://www.pulumi.com/blog/yaml-terraform-pulumi-whats-the-smart-choice-for-deployment-automation-with-kubernetes/"&gt;YAML, Terraform, Pulumi: what&amp;rsquo;s the smart choice for deployment automation with Kubernetes&lt;/a&gt;, and the &lt;a href="https://www.pulumi.com/blog/beyond-yaml-kubernetes-2026-automation-era/"&gt;beyond YAML&lt;/a&gt; write-up on where Kubernetes automation is heading in 2026.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/2P8JLgAc5QI?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;figcaption&gt;
&lt;center&gt;
&lt;i&gt;
See how Pulumi helps you manage Kubernetes Services and Deployments with code instead of YAML—bringing structure, reuse, and type safety to your configs.
&lt;/i&gt;
&lt;/center&gt;
&lt;/figcaption&gt;
&lt;p&gt;By adopting Pulumi, you can avoid the complexity of juggling endless YAML files and gain a more streamlined, maintainable workflow for your Kubernetes infrastructure.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;div style="display: flex; align-items: center; justify-content: center; height: 100%;"&gt;
&lt;img src="img_2.png" alt="A meme featuring a man slamming a card on a table, labeled “ME”, using “KUBERNETES” to solve “ANY PROBLEM TO SOLVE”, humorously depicting overuse of Kubernetes." style="width: 60%; height: 100%"&gt;
&lt;/div&gt;
&lt;p&gt;Kubernetes rewards discipline. The 20 practices above — resource hygiene, namespaced isolation, RBAC, NetworkPolicy, Pod Security, external secrets, probes, the Gateway API, observability, GitOps, policy-as-code, signed images and SBOMs, autoscaling, FinOps, multi-cluster strategy, and IaC — are the dial settings that separate clusters that page their owners every week from clusters that don&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Pick the three weakest spots in your environment and fix those first. Then promote the fixes through code, not through &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Want to learn how to put these practices into action? Meet us at &lt;a href="https://www.pulumi.com/kubecon/"&gt;KubeCon Europe 2026 (Booth 784)&lt;/a&gt; or register for our upcoming &lt;a href="https://www.pulumi.com/events/from-zero-to-production-in-kubernetes/"&gt;Zero to Production in Kubernetes&lt;/a&gt; workshop.&lt;/p&gt;
&lt;a
href="https://www.pulumi.com/docs/get-started/"
class="btn btn-primary"
&gt;
Try Pulumi for Free
&lt;/a&gt;</description><author>Engin Diri</author><category>kubernetes</category><category>best-practices</category><category>devops</category></item><item><title>Your Perfect Infrastructure May Not Be So Perfect</title><link>https://www.pulumi.com/blog/your-perfect-infrastructure/</link><pubDate>Mon, 16 Dec 2024 10:43:07 +0000</pubDate><guid>https://www.pulumi.com/blog/your-perfect-infrastructure/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/your-perfect-infrastructure/index.png" /&gt;
&lt;p&gt;&lt;strong&gt;Guest Article:&lt;/strong&gt; &lt;em&gt;Simen A. W. Olsen from &lt;a href="https://bjerk.io"&gt;Bjerk&lt;/a&gt;, is here to share his lessons learned on why designing the perfect architecture for your future needs might be a mistake&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I remember standing in front of our engineering team in 2018, proudly presenting what I believed was the future-proof architectural design for our new distributed system. The diagrams were immaculate, the technology choices were cutting-edge, and the scalability patterns were ready for any possible future scenario.&lt;/p&gt;
&lt;p&gt;I was basically the Leonardo da Vinci of system design… if Leonardo had been really into Kubernetes and had a concerning addiction to coffee. But six months later, that “future-proof” architecture had become a constraint rather than an enabler, and my masterpiece was looking more like a finger painting done by a caffeinated raccoon.&lt;/p&gt;
&lt;p&gt;This experience taught me something crucial: trying to build the perfect system that anticipates every future need is often worse than creating a system designed to change quickly. It’s like trying to predict what your kid will want to be when they grow up and pre-buying all the necessary equipment. Congrats, you now own a space suit, a stethoscope, and a dragon costume — and they decided to become a software engineer anyway.&lt;/p&gt;
&lt;h2 id="the-over-planning"&gt;The Over-Planning&lt;/h2&gt;
&lt;p&gt;Many teams fall into a common trap: they try to design systems that anticipate every possible future requirement. This happens even in agile teams, where we convince ourselves we need to “get the architecture right” before we can start iterating. You know, because nothing says “agile” like spending three months in a room drawing boxes and arrows while muttering “microservices” under your breath like it’s a magic spell.&lt;/p&gt;
&lt;p&gt;In 2008, Netflix faced a choice: build the perfect data center that could handle all their anticipated future needs, or move to the cloud with a simpler architecture that could evolve. They chose the latter, focusing on making their system easy to change rather than trying to make it perfect. Smart move — unlike those my past self made who probably would’ve insisted on building a data center capable of streaming to Mars, just in case Elon asked nicely.&lt;/p&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/p3-some-assembly-required/"&gt;
Pulumi Patterns and Practices Platform (P3): Some Assembly Required
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/pulumi-patterns-and-practices/"&gt;
Pulumi Patterns and Practices Platform (P3): A reference architecture for large-scale organizations
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/next-level-iac-briding-the-declarative-gap/"&gt;
Next-level IaC: Bridging the Declarative Gap
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-core-principles-of-change-ready-architecture"&gt;The Core Principles of Change-Ready Architecture&lt;/h2&gt;
&lt;p&gt;Through both failures and successes, I’ve identified three principles that define truly adaptable architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embrace simplicity.&lt;/strong&gt; I think it makes sense to start with the simplest architecture that could possibly work for your current needs. Complexity should be earned, not presumed. If your architecture diagram looks like a plate of spaghetti that’s been hit by lightning, you might be doing it wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make change cheap.&lt;/strong&gt; Instead of trying to avoid change, make it inexpensive. This means investing in automated testing, continuous deployment, and monitoring. When change is cheap, you don’t need to fear it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn through action.&lt;/strong&gt; Rather than trying to predict the future, build mechanisms that help you learn quickly about real needs. It includes feature toggles (Protip: Try &lt;a href="https://www.getunleash.io/"&gt;Unleash&lt;/a&gt;.), A/B testing, and robust monitoring of how your system is actually being used. You know, actual data, not just what that one loud guy in planning insists will definitely happen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rise of AI and machine learning systems has made one thing clear: we can’t predict how our systems will need to evolve. The most successful teams aren’t those that try to build the perfect AI architecture upfront, but those that can rapidly experiment and adapt their systems based on real-world feedback.&lt;/p&gt;
&lt;p&gt;The biggest pushback I hear is, “But what if we need to scale?” or “What about future requirements?” These fears often drive teams to over-architect their solutions. But here’s the reality: the cost of changing a simple system is usually lower than the cost of maintaining an over-engineered one. The key is understanding that good architecture isn’t about predicting the future — it’s about making future changes as painless as possible.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That over-engineered system I was so proud of in 2018? Its most significant flaw wasn’t in what it got wrong about the future — it was that it tried too hard to be right about the future in the first place. It’s like bringing a fully packed suitcase to a first date. Today, I know that the best architecture isn’t one that anticipates every need, but one that makes it easy to respond to needs as they emerge.&lt;/p&gt;
&lt;p&gt;The next time you’re tempted to design for every possible future scenario, remember: the goal isn’t to build a perfect system, but to build one that’s perfectly easy to change. And if someone tells you they’ve designed the perfect future-proof architecture, they’re either lying, or they’ve discovered time travel — and in that case, they should be sharing lottery numbers, not system designs.&lt;/p&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/p3-some-assembly-required/"&gt;
Pulumi Patterns and Practices Platform (P3): Some Assembly Required
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/pulumi-patterns-and-practices/"&gt;
Pulumi Patterns and Practices Platform (P3): A reference architecture for large-scale organizations
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/next-level-iac-briding-the-declarative-gap/"&gt;
Next-level IaC: Bridging the Declarative Gap
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;</description><author>Simen A. W. Olsen</author><category>architecture</category><category>developer-first-infrastructure</category><category>best-practices</category><category>cloud-engineering</category><category>cloud-deployment</category><category>developer-experience</category><category>people-ops</category><category>application-scalability</category></item><item><title>IaC Best Practices: Summarizing Key Learnings</title><link>https://www.pulumi.com/blog/iac-best-practices-summarizing-key-learnings/</link><pubDate>Mon, 08 Apr 2024 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-summarizing-key-learnings/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-summarizing-key-learnings/index.png" /&gt;
&lt;p&gt;Welcome to the final post in our series of articles focused on Infrastructure as Code best practices. In this post, we&amp;rsquo;ll wrap up our recommendations for &lt;a href="https://www.pulumi.com/product/infrastructure-as-code/"&gt;IaC with Pulumi&lt;/a&gt;, summarizing the best practices from previous posts as well as highlighting some areas of potential future growth for the team at Zephyr Archaeotech Emporium&amp;mdash;the fictional company at the center of the story throughout this series.&lt;/p&gt;
&lt;h2 id="recapping-iac-best-practices"&gt;Recapping IaC Best Practices&lt;/h2&gt;
&lt;p&gt;As this series has progressed, we&amp;rsquo;ve been showing you how Zephyr&amp;rsquo;s use of Pulumi has changed in response to how the organization, the team, and the application has changed. In case you haven&amp;rsquo;t been following along&amp;mdash;or in case you need a refresher&amp;mdash;here&amp;rsquo;s a brief summary of what&amp;rsquo;s happened in the series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zephyr started with a single project in a single repository with two stacks, one for production and one for testing. &lt;em&gt;(Details are available in &lt;a href="https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/"&gt;the first blog post&lt;/a&gt; in the series.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Zephyr quickly added per-developer stacks as a way to enhance developer productivity. &lt;em&gt;(You can read about per-developer stacks in &lt;a href="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/"&gt;the second post&lt;/a&gt; in the series.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;As Zephyr continued to grow, their single Pulumi project grew into three different Pulumi projects: one for base infrastructure, one for their Kubernetes platform, and one for the online store application. &lt;em&gt;(The &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;third post in the series&lt;/a&gt; describes the reasoning for adopting multiple projects.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Accompanying the switch to multiple projects, Zephyr implemented stack references (to pass information between stacks in different projects) and applied role-based access control (RBAC) to the stacks in Pulumi Cloud. &lt;em&gt;(Refer back to &lt;a href="https://www.pulumi.com/blog/iac-best-practices-applying-stack-references/"&gt;the fourth post&lt;/a&gt; and &lt;a href="https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/"&gt;the fifth post&lt;/a&gt; in the series for more details on each of these areas.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Zephyr realized a need to split out their data layer, going from three projects to four projects, and along the way saw an opportunity to add a higher level of orchestration using the Pulumi Automation API. &lt;em&gt;(Refer back to &lt;a href="https://www.pulumi.com/blog/iac-best-practices-using-automation-api/"&gt;our sixth post&lt;/a&gt; for more details on Zephyr&amp;rsquo;s initial use of Automation API.)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since the last blog post, the Zephyr team has continued to explore &lt;a href="https://www.pulumi.com/docs/using-pulumi/automation-api/"&gt;Automation API&lt;/a&gt;, and has added an Automation API program that allows their developers to deploy the Zephyr online store directly from the associated GitHub repositories&amp;mdash;developers don&amp;rsquo;t even need to clone down the repositories first.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;To see the latest Automation API programs for Zephyr, refer to &lt;a href="https://github.com/pulumi-zephyr/zephyr-automation"&gt;the &lt;code&gt;zephyr-automation&lt;/code&gt; GitHub repository&lt;/a&gt;.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="areas-of-future-growth-for-zephyr"&gt;Areas of future growth for Zephyr&lt;/h2&gt;
&lt;p&gt;We&amp;rsquo;ve stated before that best practices are always point-in-time recommendations based on the requirements of the organization/team/individual using Pulumi. What this means, in practice, is that there is almost always room for growth and evolution. Organizations, teams, and individuals rarely remain static, and therefore their use of Pulumi is also unlikely to remain entirely static.&lt;/p&gt;
&lt;p&gt;So, what are some potential areas of future growth for Zephyr?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Splitting the deployment of the microservices&lt;/strong&gt;: We alluded to this &lt;a href="https://www.pulumi.com/blog/iac-best-practices-using-automation-api/"&gt;in the previous post&lt;/a&gt;. Right now, all the microservices that comprise the online store are deployed together. What if one particular team needs to deploy only their service? The current approach can certainly accommodate that&amp;mdash;Pulumi is declarative and will only change what needs to be changed&amp;mdash;but what&amp;rsquo;s better for the Zephyr team? Does Zephyr need to optimize for allowing service teams to move independently? Is it about reducing blast radius, so that undesired changes aren&amp;rsquo;t introduced to another service? These are the sorts of considerations that drive this decision (and the third post in our series discusses &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;structuring Pulumi projects&lt;/a&gt; in more detail).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Using component resources&lt;/strong&gt;: Currently, the Zephyr team is using the standard API objects supplied by the Pulumi provider. For example, the various microservices for the Zephyr online store are defined using standard Kubernetes objects, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsNs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Namespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;assets-ns&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;: &lt;span class="kt"&gt;...&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;assets-service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;: &lt;span class="kt"&gt;...&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsDeployment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;assets-deployment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;: &lt;span class="kt"&gt;...&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://www.pulumi.com/docs/concepts/resources/components/"&gt;Component resources&lt;/a&gt; would allow the Zephyr team to define a single logical resource that contains &amp;ldquo;child&amp;rdquo; resources. This would allow the Zephyr team to create a &lt;code&gt;Microservice&lt;/code&gt; resource that incorporates the standard Kubernetes &lt;code&gt;Namespace&lt;/code&gt;, &lt;code&gt;Deployment&lt;/code&gt;, and &lt;code&gt;Service&lt;/code&gt; objects. Deploying an instance of a &lt;code&gt;Microservice&lt;/code&gt; resource deploys all the child objects defined in the component resource. This can simplify the Pulumi code through further use of &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY (Don&amp;rsquo;t Repeat Yourself)&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Making use of Pulumi Deployments&lt;/strong&gt;: Zephyr&amp;rsquo;s CI/CD pipelines are not a topic we&amp;rsquo;ve discussed in the series. Given that Zephyr uses GitHub, they could use GitHub Actions (GHA); Pulumi has &lt;a href="https://github.com/pulumi/actions"&gt;an action for GHA workflows&lt;/a&gt;. Zephyr could also use &lt;a href="https://www.pulumi.com/product/pulumi-deployments/"&gt;Pulumi Deployments&lt;/a&gt;, which offers an extensive set of features&amp;mdash;OIDC support for dynamic credentials, Review Stacks (per-PR stacks), support for GitHub Enterprise, and GitOps-style workflows, to name a few. Support for Pulumi Deployments is also baked into Automation API, meaning that Zephyr could extend their existing Automation API code to incorporate Pulumi Deployments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Addressing secrets and configuration management&lt;/strong&gt;: We&amp;rsquo;ve also not discussed how Zephyr could manage their secrets and configuration data. &lt;a href="https://www.pulumi.com/product/esc/"&gt;Pulumi ESC&lt;/a&gt; is a good option here, offering seamless integration with Pulumi for infrastructure as code while also supporting a variety of &lt;a href="https://www.pulumi.com/what-is/infrastructure-as-code-for-devops/"&gt;other DevOps tools and use cases&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even though it&amp;rsquo;s working well, clearly there&amp;rsquo;s a lot of room for the Zephyr team to continue to enhance and improve their infrastructure as code implementation!&lt;/p&gt;
&lt;h2 id="summarizing-best-practices"&gt;Summarizing Best Practices&lt;/h2&gt;
&lt;p&gt;Throughout the series, we&amp;rsquo;ve provided guidelines on how to best utilize Pulumi. While we recommend reading the entire series, in this section will &amp;ldquo;summarize our summaries&amp;rdquo; and provide some broad, overarching practices.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;With regard to Pulumi projects, &lt;strong&gt;keep it as simple as possible, but no simpler.&lt;/strong&gt; While tools like Automation API can help with orchestrating multiple projects, it remains true that in most cases a single project is simpler. Use multiple projects when necessary, but only when necessary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use stacks freely.&lt;/strong&gt; Using multiple projects has a cost in complexity. Using multiple stacks, on the other hand, does not&amp;mdash;so use them freely to model different environments, to provide per-developer test environments, to test changes before rolling them into production, or for any other use case where you need a separate, independent instance of the infrastructure defined in your Pulumi program.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focus on the requirements.&lt;/strong&gt; It can sometimes be too easy to focus on the technology instead of the requirements. There are a &lt;em&gt;lot&lt;/em&gt; of very cool things you can do with Pulumi, but try not to get distracted from the reason you&amp;rsquo;re using infrastructure as code: repeatable, reliable, consistent deployments of your cloud infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t forget about security along the way.&lt;/strong&gt; Make sure you are properly securing your infrastructure. Pulumi &lt;a href="https://www.pulumi.com/crossguard"&gt;CrossGuard&lt;/a&gt; can help here by providing &amp;ldquo;policy as code&amp;rdquo; guardrails for cloud infrastructure. Pulumi Cloud&amp;rsquo;s RBAC functionality helps with ensuring the right users have the right permissions to the right sets of cloud infrastructure resources.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="reviewing-the-code"&gt;Reviewing the Code&lt;/h2&gt;
&lt;p&gt;You can view the code we&amp;rsquo;ve presented in this series in the following GitHub repositories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/pulumi-zephyr/zephyr-infra"&gt;&amp;ldquo;zephyr-infra&amp;rdquo; repository&lt;/a&gt; contains the base infrastructure definitions.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/pulumi-zephyr/zephyr-k8s"&gt;&amp;ldquo;zephyr-k8s&amp;rdquo; repository&lt;/a&gt; contains the Pulumi code to stand up the Kubernetes cluster onto which Zephyr deploys their online store application.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/pulumi-zephyr/zephyr-data"&gt;&amp;ldquo;zephyr-data&amp;rdquo; repository&lt;/a&gt; manages Zephyr&amp;rsquo;s &amp;ldquo;data layer&amp;rdquo; for persistent data used by the online store application.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/pulumi-zephyr/zephyr-app"&gt;&amp;ldquo;zephyr-app&amp;rdquo; repository&lt;/a&gt; is Zephyr&amp;rsquo;s online store. This repository has both the application code as well as the Pulumi code for deploying the application (see the &amp;ldquo;infra&amp;rdquo; directory).&lt;/li&gt;
&lt;li&gt;In the &lt;a href="https://github.com/pulumi-zephyr/zephyr-automation"&gt;&amp;ldquo;zephyr-automation&amp;rdquo; repository&lt;/a&gt; you&amp;rsquo;ll find Automation API programs used by Zephr to orchestrate deployments of the different projects involved in supporting the online store.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each of these repositories, there are different branches that reflect the state of the code as of a particular blog post. The &amp;ldquo;main&amp;rdquo; branch reflects the final state of the code, as described in this wrap-up post.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;This series has used the story of a fictional company, Zephyr Archaeotech Emporium, to discuss some recommendations around how to best utilize Pulumi for solving their infrastructure as code needs. While this company is fictional, the benefits of Pulumi and infrastructure as code are real. We invite you to try for yourself&amp;mdash;sign up for &lt;a href="https://app.pulumi.com/signup"&gt;a free Pulumi Cloud account&lt;/a&gt; today!&lt;/p&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/your-perfect-infrastructure/"&gt;
Your Perfect Infrastructure May Not Be So Perfect
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;</description><author>Scott Lowe</author><category>best-practices</category></item><item><title>IaC Best Practices: Using Automation API</title><link>https://www.pulumi.com/blog/iac-best-practices-using-automation-api/</link><pubDate>Wed, 26 Jul 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-using-automation-api/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-using-automation-api/index.png" /&gt;
&lt;p&gt;Welcome to the sixth post in our series of blog posts focused on Infrastructure as Code (IaC) best practices. So far in this series, you&amp;rsquo;ve seen how Zephyr Archaeotech Emporium&amp;mdash;the fictional company at the center of this series&amp;mdash;uses Pulumi to manage their online retail store. You read how Zephyr&amp;rsquo;s initial use of Pulumi changed to use short-lived per-developer stacks. Later, as Zephyr continued to grow, you saw how Zephyr restructured their Pulumi projects and stacks, incorporated Stack References, and used Pulumi Cloud&amp;rsquo;s role-based access control (RBAC) functionality to control access to their stacks. This post focuses on how Zephyr takes advantage of the &lt;a href="https://www.pulumi.com/docs/using-pulumi/automation-api/concepts-terminology"&gt;Pulumi Automation API&lt;/a&gt; to bring an even greater level of orchestration to the stacks that represent their online store.&lt;/p&gt;
&lt;p&gt;The ultimate goal of this series is to discuss best practices for using Pulumi to manage a fairly complex containerized application. These practices have unfolded organically, as a direct response to Zephyr&amp;rsquo;s evolving needs. The aim is to demonstrate that best practices are not set in stone, but are rather &amp;ldquo;point in time&amp;rdquo; recommendations that adapt as your company grows.&lt;/p&gt;
&lt;h2 id="managing-complexity-in-a-multi-project-pulumi-setup"&gt;Managing Complexity in a Multi-Project Pulumi Setup&lt;/h2&gt;
&lt;p&gt;Over the course of this series, you&amp;rsquo;ve seen how Zephyr&amp;rsquo;s use of Pulumi has grown and changed. For the sake of completeness, here&amp;rsquo;s a quick recap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zephyr started with a single project in a single repository with two stacks, one for production and one for testing. &lt;em&gt;(Details are available in &lt;a href="https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/"&gt;understanding code organization and stacks&lt;/a&gt; post.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;To unlock developer velocity, Zephyr quickly added per-developer stacks. &lt;em&gt;(You can read about &lt;a href="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/"&gt;per-developer stacks&lt;/a&gt; post in the series.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;As Zephyr grew and expanded, their single Pulumi project grew into three different Pulumi projects: one for base infrastructure, one for their Kubernetes platform, and one for the online store application. &lt;em&gt;(The &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;structuring Pulumi projects&lt;/a&gt; post describes the reasoning for adopting multiple projects.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Accompanying the switch to multiple projects, Zephyr implemented stack references (to pass information between stacks in different projects) and applied role-based access control (RBAC) to the stacks in Pulumi Cloud. &lt;em&gt;(Refer back to the &lt;a href="https://www.pulumi.com/blog/iac-best-practices-applying-stack-references/"&gt;stack references&lt;/a&gt; and &lt;a href="https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/"&gt;RBAC &amp;amp; security&lt;/a&gt; posts in the series for more details on each of these areas.)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;To see the state of Zephyr&amp;rsquo;s Pulumi implementation before the changes described in this article, please look at the &lt;code&gt;blog/multi-project&lt;/code&gt; branch in each of the GitHub repositories (&lt;code&gt;zephyr-infra&lt;/code&gt;, &lt;code&gt;zephyr-k8s&lt;/code&gt;, and &lt;code&gt;zephyr-app&lt;/code&gt;).&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Since we last checked in on Zephyr, one significant change has emerged. As traffic to the Zephyr online store continued to increase, Zephyr&amp;rsquo;s operations team was finding it difficult to scale the relational databases running on Kubernetes (specifically, the orders and catalog databases). After evaluating their options, the team decided to migrate to &lt;a href="https://aws.amazon.com/rds/aurora/"&gt;Amazon Aurora&lt;/a&gt; with MySQL compatibility. Using the considerations outlined for structuring Pulumi projects, they decided to create a fourth Pulumi project, named &amp;ldquo;zephyr-data&amp;rdquo;, to manage the data infrastructure for the online store. Initially, this data infrastructure project manages only the Aurora clusters and database instances for the orders and catalog databases.&lt;/p&gt;
&lt;p&gt;Because Zephyr was already using a modular approach to their overall application stack, inserting a new project for managing the online store&amp;rsquo;s data layer involved only minimal changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new Pulumi program had to be written. This new program used stack references to leverage the existing infrastructure created by the base infrastructure stack and allowed traffic to the databases from the security group created by the Kubernetes platform stack. You&amp;rsquo;ll find this new program in &lt;a href="https://github.com/pulumi/zephyr-data/"&gt;the &lt;code&gt;zephyr-data&lt;/code&gt; repository&lt;/a&gt; on GitHub (look in the &lt;code&gt;blog/automation-api&lt;/code&gt; branch).&lt;/li&gt;
&lt;li&gt;The Pulumi program for the Kubernetes platform stack only needed to export the security group created for the cluster nodes.&lt;/li&gt;
&lt;li&gt;The Pulumi program for deploying the online store microservices referenced the database endpoints created by the new data infrastructure stack, and no longer spun up containerized instances of MariaDB for the orders and catalog databases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The resulting four project architecture looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="four-project-architecture.jpg" alt="An illustration of Zephyr&amp;rsquo;s four project architecture"&gt;&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;To see the state of the code after the changes described above&amp;mdash;the addition of the &lt;code&gt;zephyr-data&lt;/code&gt; project and the changes to use Amazon Aurora&amp;mdash;please review the &lt;code&gt;blog/automation-api&lt;/code&gt; branch in each of the Zephyr repositories.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Using a modular approach with multiple projects has its benefits. However, one of the trade-offs of splitting your IaC code into separate Pulumi projects is some additional relative complexity. With additional complexity comes additional room for error. To stand up a production-like environment, developers now had to stand up four different stacks (base infrastructure, Kubernetes platform, data infrastructure, and online store) in a specific order. If a user gets the order wrong, then the online store won&amp;rsquo;t come up properly.&lt;/p&gt;
&lt;p&gt;With only four projects, this is not an insurmountable challenge by any stretch. However, what if it was 10 projects? Or 15 projects? Those numbers are not out of reach! Right now Zephyr has a single project to deploy all the various services involved for the online store. What if the application team decides to split up that project so that individual services can be deployed independently of each other? (After all, being able to deploy microservices independently of one another is one of the benefits of such an architecture.)&lt;/p&gt;
&lt;p&gt;The Zephyr team started asking some questions: How can we make this process simpler? How can we remove room for error? How can we prepare our teams for greater scale?&lt;/p&gt;
&lt;h2 id="simplifying-advanced-deployments-with-automation-api"&gt;Simplifying Advanced Deployments with Automation API&lt;/h2&gt;
&lt;p&gt;The challenges facing the Zephyr teams are not unique to Zephyr. Many IaC vendors recommend using a modular approach. Unfortunately, most IaC products rely on less-than-ideal approaches&amp;mdash;like Bash scripts or Makefiles&amp;mdash;to &amp;ldquo;stitch&amp;rdquo; together the different modules.&lt;/p&gt;
&lt;p&gt;In some cases, organizations can use existing build automation tools and CI/CD pipelines to assist. What&amp;rsquo;s really needed, though, is a way to programmatically control the IaC tooling, and that&amp;rsquo;s what Pulumi&amp;rsquo;s Automation API is. The Pulumi Automation API allows Zephyr&amp;rsquo;s teams to use a general purpose programming language to automate Pulumi operations like performing an update, refreshing the stack state, or destroying a stack. Like Pulumi itself, the Pulumi Automation API supports multiple languages and supports multiple state backends, including Pulumi Cloud. Further, there is &lt;strong&gt;no requirement&lt;/strong&gt; that Pulumi Automation API programs are written in the same language as the Pulumi programs they automate. In the case of Zephyr, which thus far has chosen to use TypeScript for their Pulumi programs, this means they are free to use any supported language for their Pulumi Automation API programs.&lt;/p&gt;
&lt;p&gt;When the Zephyr team started evaluating options for simplifying the deployment of their online store and associated infrastructure, they decided to move forward with the Pulumi Automation API for these reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tight integration with Pulumi and Pulumi Cloud&lt;/li&gt;
&lt;li&gt;Support for Pulumi Deployments (something we&amp;rsquo;ll examine in the next blog post)&lt;/li&gt;
&lt;li&gt;Support for a broad range of programming languages, just like Pulumi itself&lt;/li&gt;
&lt;li&gt;No requirement to use the same language as the Pulumi programs being automated (in fact, a Pulumi Automation API program could automate multiple programs written in different supported languages!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at Zephyr&amp;rsquo;s initial foray into using Pulumi Automation API.&lt;/p&gt;
&lt;h2 id="performing-stack-operations-efficiently"&gt;Performing Stack Operations Efficiently&lt;/h2&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;You can view the Pulumi Automation API program that Zephyr wrote in the &lt;code&gt;local-source&lt;/code&gt; directory of &lt;a href="https://github.com/pulumi/zephyr-automation/"&gt;the &lt;code&gt;pulumi/zephyr-automation&lt;/code&gt; repository&lt;/a&gt; on GitHub, in the &lt;code&gt;blog/automation-api&lt;/code&gt; branch.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Zephyr&amp;rsquo;s team decided to write their Pulumi Automation API program in Golang. One of the first things you&amp;rsquo;ll note about the Pulumi Automation API program is that it looks more like a &amp;ldquo;regular&amp;rdquo; Go program than a typical Pulumi Go program.&lt;/p&gt;
&lt;p&gt;For example, a typical Pulumi Automation API program written in Go starts out like any other Go program:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// All your Go code is here&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In a typical Pulumi Go program, on the other hand, the &lt;code&gt;main()&lt;/code&gt; function starts off with a &lt;code&gt;pulumi.Run&lt;/code&gt; call, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// All the Pulumi Go code is here&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The easiest way to understand this difference is to consider the way each program is invoked. A Pulumi Automation API program written in Go is executed by running &lt;code&gt;go run main.go&lt;/code&gt;, like running any other Go program. In contrast, a Pulumi Go program is invoked by the &lt;code&gt;pulumi&lt;/code&gt; CLI.&lt;/p&gt;
&lt;p&gt;Zephyr&amp;rsquo;s Automation API program &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L34-L40"&gt;starts out&lt;/a&gt; with checking for a command-line argument to indicate the desired mode of the operation&amp;mdash;should it be creating/updating resources, or destroying resources? In a standard Pulumi Go program, this would be determined by the invocation of the &lt;code&gt;pulumi&lt;/code&gt; CLI (for example, whether you ran &lt;code&gt;pulumi up&lt;/code&gt; or &lt;code&gt;pulumi destroy&lt;/code&gt;). Here, though, that falls to the user to check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Determine mode of operation; default is to refresh/update&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;destroy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;argsWithoutProg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argsWithoutProg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;argsWithoutProg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;destroy&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;destroy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, the program &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L42-L53"&gt;reads some configuration data&lt;/a&gt; from a YAML configuration file; this configuration data includes things like the relative file system location of the Pulumi programs that this Automation API program is going to execute:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Get configuration data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;config.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Error reading configuration file: %v\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Environment&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Error parsing configuration information: %v\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Separating configuration from code in this manner allows you to use the language&amp;rsquo;s existing ability to create binary executables that can be easily distributed to your teams. For example, Zephyr has the option of running &lt;code&gt;go build&lt;/code&gt; to create a binary executable for distribution to internal teams.&lt;/p&gt;
&lt;p&gt;From there, the Automation API program gets into &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L58-L92"&gt;setting up the stacks&lt;/a&gt; it will use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Set up base stack&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;baseStackName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullyQualifiedStackName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BaseProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpsertStackLocalSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseStackName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BaseProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Failed to create or select %s stack: %v\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BaseProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Nickname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Successfully created/selected %s stack\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BaseProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Nickname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this particular program, Zephyr decided to start with a &lt;em&gt;local workspace.&lt;/em&gt; (You can read more about workspaces in the &lt;a href="https://www.pulumi.com/docs/using-pulumi/automation-api/getting-started-automation-api/#associate-with-a-stack"&gt;Pulumi Automation API docs&lt;/a&gt;.) This is reflected in the use of the &lt;code&gt;UpsertStackLocalSource&lt;/code&gt; method shown above, which will create or select a stack for a Pulumi program found in a local filesystem location.&lt;/p&gt;
&lt;p&gt;Users can also use &lt;code&gt;UpsertStackInlineSource&lt;/code&gt;, which embeds the Pulumi program into the Automation API program as a function. There are trade-offs to each approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using local source (i.e., the Pulumi program is stored outside of the Automation API program) has the benefit of allowing users to independently revise the Pulumi program or the Automation API program; this may be the ideal approach where organizations have separate teams that will create, manage, or maintain the Automation API programs. In Zephyr&amp;rsquo;s case, where there is a platform team, this made a lot of sense for them. This does also mean, as you might guess, that the Pulumi programs must be reachable by the Automation API program (this typically means users need to clone down the appropriate repositories to their system first).&lt;/li&gt;
&lt;li&gt;Using inline source (where the Pulumi program is embedded in the Automation API program) may be a better fit when there is a single team managing or maintaining both the Pulumi program(s) as well as the Automation API program(s), or when&amp;mdash;for whatever reason&amp;mdash;these programs should not be revised independently of one another. This has the benefit of removing the need for users to clone or copy separate Pulumi programs onto their system before being able to use the Automation API program.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After all four stacks&amp;mdash;base, platform, data, and app&amp;mdash;have been set up for use within the Pulumi Automation API program, then comes the &amp;ldquo;real work&amp;rdquo;: performing operations on those stacks. To reduce duplicate code, the Zephyr program makes use of three functions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;refreshStack&lt;/code&gt; function (starting on &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L199-L216"&gt;line 199&lt;/a&gt; of &lt;code&gt;main.go&lt;/code&gt;) performs the equivalent of a &lt;code&gt;pulumi refresh&lt;/code&gt; against a stack.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;updateStack&lt;/code&gt; function (starting on &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L218-L234"&gt;line 218&lt;/a&gt; of &lt;code&gt;main.go&lt;/code&gt;) performs an update against the stack. This is like running &lt;code&gt;pulumi up&lt;/code&gt; against a stack.&lt;/li&gt;
&lt;li&gt;Finally, the &lt;code&gt;deleteStack&lt;/code&gt; function (starting on &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L236-L252"&gt;line 236&lt;/a&gt; of &lt;code&gt;main.go&lt;/code&gt;) destroys the resources in a stack. This equates to running &lt;code&gt;pulumi destroy&lt;/code&gt; against a stack.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Using these functions, &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L94-L197"&gt;lines 94 through 197&lt;/a&gt; perform the key task of taking action to stand up or tear down the Zephyr online store and all associated infrastructure. If the user passes the parameter &amp;ldquo;destroy&amp;rdquo; on the command line (meaning the Pulumi Automation API program is invoked using &lt;code&gt;go run main.go destroy&lt;/code&gt;), then it will tear down the stacks. Otherwise, the Pulumi Automation API program will perform a refresh and then an update&amp;mdash;much the same way that the &lt;code&gt;pulumi&lt;/code&gt; CLI itself behaves.&lt;/p&gt;
&lt;p&gt;Along the way, you can see the Pulumi Automation API program passing configuration values to the stacks being orchestrated (this is &lt;a href="https://github.com/pulumi/zephyr-automation/blob/blog/automation-api/local-source/main.go#L177-L182"&gt;lines 177 through 182&lt;/a&gt;, for example):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Set config values for app stack&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;platformOrgName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dataOrgName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;platformProjName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlatformProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dataProjName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DataProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;platformStackName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dataStackName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConfigValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Among other things, this means you can take your existing Pulumi programs, and&amp;mdash;without making any changes&amp;mdash;orchestrate them using an Automation API program! Further, in reviewing the code shown above, you can see that the Pulumi Automation API ties into &lt;a href="https://www.pulumi.com/docs/concepts/secrets/"&gt;Pulumi&amp;rsquo;s secrets functionality&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the end, using Pulumi&amp;rsquo;s Automation API enabled Zephyr&amp;rsquo;s platform team to simplify the workflow for standing up the online store application and all of its associated infrastructure into a simple and straightforward process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Developers ensure they have cloned the appropriate source repositories onto their system.&lt;/li&gt;
&lt;li&gt;Developers run &lt;code&gt;go run main.go&lt;/code&gt; (or &lt;code&gt;go run main.go destroy&lt;/code&gt;) to stand up or tear down the online store and all associated infrastructure.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Although Zephyr uses multiple GitHub repositories, the Pulumi Automation API has no requirements for multiple repositories. You could use a monorepo, if that better suits your particular requirements.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="summarizing-best-practices-automation-api"&gt;Summarizing Best Practices: Automation API&lt;/h2&gt;
&lt;p&gt;This post discussed the following best practices when using Pulumi Automation API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use the Automation API to orchestrate operations on multiple stacks.&lt;/strong&gt; In cases where you&amp;rsquo;ve split Pulumi projects (per the recommendations in the post on &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;structuring Pulumi projects&lt;/a&gt;), you&amp;rsquo;ve introduced a potential opportunity for using Automation API to orchestrate operations. This is a great counter-balance to offset the relative increase in complexity that comes from breaking large Pulumi projects into smaller, more focused Pulumi projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate configuration from code.&lt;/strong&gt; This is already a generally-accepted recommendation in most programming circles, and it applies to Automation API programs as well. Doing so enables you, if you wish, to distribute binary artifacts to teams instead of requiring them to execute the source code of your Automation API programs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use local source to decouple Automation API programs from Pulumi programs when separate teams are involved.&lt;/strong&gt; When separate teams are creating, managing, or maintaining the Automation API programs and the Pulumi programs, using local source instead of inline source enables the teams to operate somewhat independently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When there is a single team managing both Automation API programs and Pulumi programs, use inline source.&lt;/strong&gt; This is also true when, perhaps due to compliance or regulatory concerns, the Automation API program should be more tightly coupled to the Pulumi programs it is automating.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Zephyr series wraps up in the next post, where we summarize all our recommendations so far. We&amp;rsquo;ll also take a look at areas of potential future growth for the team at Zephyr. Don&amp;rsquo;t miss it!&lt;/p&gt;</description><author>Scott Lowe</author><category>best-practices</category><category>automation-api</category><category>go</category></item><item><title>IaC Best Practices: Implementing RBAC and Security</title><link>https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/</link><pubDate>Tue, 23 May 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-implementing-rbac-and-security/index.png" /&gt;
&lt;p&gt;This post continues our series of blog posts focused on IaC best practices. In earlier posts, we introduced Zephyr Archaeotech Emporium, the fictional company that sits at the center of this series, and discussed Zephyr&amp;rsquo;s primary use case for Pulumi: managing their online retail store. You read how Zephyr&amp;rsquo;s initial use of Pulumi changed to incorporate the use of short-lived per-developer stacks. Later, as Zephyr continued to grow, you saw how Zephyr restructured their Pulumi projects and stacks, and incorporated the use of Stack References. This post is a complement to the post on structuring Pulumi projects, concentrating on the use of role-based access control (RBAC) and security in Zephyr&amp;rsquo;s multi-project configuration.&lt;/p&gt;
&lt;p&gt;The ultimate goal of this series is to discuss best practices for using Pulumi to manage a fairly complex containerized application. You&amp;rsquo;ve seen these practices emerge over the course of the series&amp;mdash;not all immediately, and not all right away. Structuring the blog series in this way is a deliberate decision; many best practices are &amp;ldquo;point in time&amp;rdquo; recommendations: they are context-dependent and the recommendations for any given company, like Zephyr, may change as the company and its needs change.&lt;/p&gt;
&lt;h2 id="evolving-project-structure-to-enhance-security-and-scalability"&gt;Evolving Project Structure to Enhance Security and Scalability&lt;/h2&gt;
&lt;p&gt;So far, you&amp;rsquo;ve read about how Zephyr started out with a single project and a single Git repository. Adding short-lived per-developer stacks, as described in the post on &lt;a href="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/"&gt;best practices for developer stacks and Git branches&lt;/a&gt;, didn&amp;rsquo;t really impact this structure. However, as the company grew, Zephyr&amp;rsquo;s project and Git repository structure evolved into its current form:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &amp;ldquo;zephyr-infra&amp;rdquo; project handles base infrastructure (VPC and related constructs).&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;zephyr-k8s&amp;rdquo; project automates the Kubernetes platform layer, building atop the base infrastructure components supplied by the &amp;ldquo;zephyr-infra&amp;rdquo; project.&lt;/li&gt;
&lt;li&gt;Finally, the &amp;ldquo;zephyr-app&amp;rdquo; project handles the deployment of the online retail store application onto Kubernetes, layering on top of the platform provided by the &amp;ldquo;zephyr-k8s&amp;rdquo; project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can review the reasons for this structure in the post on &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;best practices for structuring Pulumi projects&lt;/a&gt;. One of the reasons listed in the third Zephyr blog post was security:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;hellip;security is a factor that affects how projects and stacks should be structured. In Zephyr&amp;rsquo;s case, the ability for one team to affect another team&amp;rsquo;s resources was present when everything was a single project. With multiple projects, Zephyr can use stack permissions and role-based access control (RBAC) in &lt;a href="https://www.pulumi.com/product/pulumi-cloud/"&gt;Pulumi Cloud&lt;/a&gt; to appropriately control &lt;em&gt;who&lt;/em&gt; can affect &lt;em&gt;what&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The last part of that quote&amp;mdash;the part about being able to &amp;ldquo;appropriately control &lt;em&gt;who&lt;/em&gt; can affect &lt;em&gt;what&lt;/em&gt;&amp;rdquo;&amp;mdash;is another way of describing what&amp;rsquo;s known as &lt;a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege"&gt;the principle of least privilege&lt;/a&gt;. If you&amp;rsquo;re unfamiliar with this term, we recommend following the link and getting more information, because the principle of least privilege stands at the core of all the recommended practices you&amp;rsquo;ll read in this post.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Pulumi supports a number of self-managed backends, including Amazon S3 (and S3-compatible servers like Minio or Ceph), Azure Blob Storage, and Google Cloud Storage. The RBAC functionality described in this post applies only to the managed Pulumi Cloud backend.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="understanding-the-rbac-building-blocks-in-pulumi-cloud"&gt;Understanding the RBAC Building Blocks in Pulumi Cloud&lt;/h2&gt;
&lt;p&gt;To understand how to secure your Pulumi projects and stacks in Pulumi Cloud, it is important to first explain the basic building blocks you&amp;rsquo;ll use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Organization:&lt;/em&gt; An organization represents a customer/company within Pulumi Cloud. Generally speaking, a single company like Zephyr is represented by a single organization.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Member:&lt;/em&gt; Each member represents a user account that is part of an organization. In Zephyr&amp;rsquo;s case, each Zephyr employee that needs to interact with Pulumi is a member of Zephyr&amp;rsquo;s organization in Pulumi Cloud.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Team:&lt;/em&gt; A team is a grouping of members, and is the primary component involved in assigning per-stack permissions in the &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/access-management/teams/"&gt;Pulumi Cloud RBAC model&lt;/a&gt;. How you structure teams within your organization will depend on a great many different factors we&amp;rsquo;ll explore in this post.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Stack:&lt;/em&gt; A stack is a separate instance of the resources created by a Pulumi program within a project. Each stack has its own independent state and its own configuration values. With respect to RBAC, each stack is a separate entity to which one or more teams can be granted permission. In Zephyr&amp;rsquo;s case, there is a long-lived production stack for each of the separate projects, and a persistent (or long-lived) test stack for each of the separate projects. There are also the per-developer stacks, which are typically short-lived.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This diagram, also used in the post on &lt;a href="https://www.pulumi.com/blog/iac-best-practices-applying-stack-references/"&gt;best practices for stack references&lt;/a&gt;, shows the stacks and projects for Zephyr&amp;rsquo;s online store:&lt;/p&gt;
&lt;p&gt;&lt;img src="gh-repos-pulumi-projects-stacks.jpg" alt="A diagram depicting how Zephyr&amp;rsquo;s projects and stacks are structured"&gt;&lt;/p&gt;
&lt;p&gt;Now, how are these different components assembled together? At its most basic level, you would use teams to group members together, and then assign stack permissions to the team. This diagram helps illustrate the idea:&lt;/p&gt;
&lt;p&gt;&lt;img src="security-stacks-teams.jpg" alt="A diagram illustrating the relationship between stacks, teams, and members"&gt;&lt;/p&gt;
&lt;p&gt;A couple of important points emerge from the above diagram:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The relationship between stacks and teams is &lt;em&gt;many-to-many&lt;/em&gt;. That is, any given stack can have many teams assigned permissions, and any given team may be assigned permissions to many stacks.&lt;/li&gt;
&lt;li&gt;Similarly, the relationship between teams and members is also &lt;em&gt;many-to-many&lt;/em&gt; (any given team can&amp;mdash;and almost always will&amp;mdash;have multiple members, and any given member can be part of multiple teams at the same time).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="assembling-security-for-stacks"&gt;Assembling Security for Stacks&lt;/h2&gt;
&lt;p&gt;The first, and perhaps the most important, best practice with regards to securing your stacks is &lt;strong&gt;to actually use Pulumi Cloud&amp;rsquo;s RBAC functionality&lt;/strong&gt;. While this may seem obvious, there are practical implications to this statement that are useful to explore.&lt;/p&gt;
&lt;p&gt;Pulumi Cloud has the ability to set a &lt;em&gt;default stack permission&lt;/em&gt; for all members of an organization. In the screenshot below, you can see that the default permission has been set to Write:&lt;/p&gt;
&lt;p&gt;&lt;img src="pulumi-cloud-stack-permissions.jpg" alt="Screenshot showing the default stack permissions for all members"&gt;&lt;/p&gt;
&lt;p&gt;In this configuration, you are effectively disabling Pulumi Cloud&amp;rsquo;s RBAC functionality. Permissions in Pulumi Cloud are &lt;em&gt;additive&lt;/em&gt;; that is, permissions only state what you can do, not what you can&amp;rsquo;t do. Therefore, if the default stack permission is set to Write, then assigning a team read-only permissions to a stack has no effect&amp;mdash;because members of that team can read from the stack (by virtue of their inclusion in a team) &lt;em&gt;and&lt;/em&gt; can write to the stack (by virtue of the default stack permission). The team members&amp;rsquo; read-only permission is &lt;em&gt;added&lt;/em&gt; to the default stack permission for their effective permission.&lt;/p&gt;
&lt;p&gt;For this reason, Pulumi recommends &lt;strong&gt;setting the default stack permission to Read or None.&lt;/strong&gt; Setting it to Read means that all members of the organization can see and read from stacks that aren&amp;rsquo;t their own, but can&amp;rsquo;t change or modify the others stacks. Setting it to None means that members have permissions to their own stacks by default&amp;mdash;gaining any form of access to stacks other than their own requires being added to a team that has permissions to those stacks.&lt;/p&gt;
&lt;p&gt;When it comes to assigning a team access to a stack, there are three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stack reader (read-only access; also called &amp;ldquo;Stack reader access&amp;rdquo; in some areas of the Pulumi Cloud UI)&lt;/li&gt;
&lt;li&gt;Stack editor (read/write access, but can&amp;rsquo;t delete the stack; also called &amp;ldquo;Stack write access&amp;rdquo; in the Pulumi Cloud UI)&lt;/li&gt;
&lt;li&gt;Stack admin (full access; you may also see it called &amp;ldquo;Stack admin access&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;When the &amp;ldquo;Allow organization members to create stacks and transfer stacks to this organization&amp;rdquo; is checked, like in the screenshot above, then the user (organization member) that creates a stack automatically becomes the stack admin for that stack. The user&amp;rsquo;s ability to delete the stack, however, is affected by the &amp;ldquo;Allow stack admins to delete stacks&amp;rdquo; setting.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Full details on what each of these permissions grants is detailed in &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/projects-and-stacks/"&gt;the projects and stacks documentation&lt;/a&gt; (see the &amp;ldquo;Stack Permissions&amp;rdquo; section). In accordance with the principle of least privilege, Pulumi recommends you &lt;strong&gt;grant the minimum level of access necessary&lt;/strong&gt; in order for a user (organization member) to meet their job responsibilities. You accomplish this by adding the member to a team, and then granting the team the minimum level of necessary access. &lt;em&gt;You can&amp;rsquo;t assign stack permissions directly to users.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Because team membership is central to the use of stack permissions, Pulumi has several ways to make it easier to manage the relationship between teams and stacks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You can use the Pulumi Cloud user interface to assign a team access to a stack.&lt;/li&gt;
&lt;li&gt;You can use Pulumi Cloud&amp;rsquo;s REST API to control team access to stacks.&lt;/li&gt;
&lt;li&gt;You can use the Pulumi Cloud provider for Pulumi to programmatically manage team access to a stack using Pulumi itself.&lt;/li&gt;
&lt;li&gt;You can use the &lt;code&gt;--teams&lt;/code&gt; flag to &lt;code&gt;pulumi stack init&lt;/code&gt; to assign stack editor access to one or more teams when you create the stack.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In general, Pulumi recommends &lt;strong&gt;declaratively managing your Pulumi Cloud RBAC configuration&lt;/strong&gt; with the Pulumi Cloud provider (formerly known as the Pulumi Service provider), unless you have specific requirements that would prevent it. However, be aware that the Pulumi Cloud provider doesn&amp;rsquo;t allow you to change a member&amp;rsquo;s role in an organization (every member is either a &amp;ldquo;member&amp;rdquo; or an &amp;ldquo;admin&amp;rdquo; within an organization); that would have to be done via the Pulumi Cloud user interface or REST API.&lt;/p&gt;
&lt;p&gt;Pulumi also recommends &lt;strong&gt;using the &lt;code&gt;--teams&lt;/code&gt; flag with &lt;code&gt;pulumi stack init&lt;/code&gt; to ensure that new stacks are properly assigned team permissions upon creation.&lt;/strong&gt; You can then go back and fine-tune those team permissions&amp;mdash;which are set to &amp;ldquo;Stack editor&amp;rdquo; or &amp;ldquo;Stack write access&amp;rdquo; when you use the &lt;code&gt;--teams&lt;/code&gt; flag&amp;mdash;using a Pulumi program with the Pulumi Cloud provider. Alternately, teams can use a team access token when creating a new stack, and that will also grant the team permission to the newly-created stack.&lt;/p&gt;
&lt;p&gt;Finally, Pulumi recommends &lt;strong&gt;allowing users to create their own stacks.&lt;/strong&gt; Otherwise, only an organization admin would be allowed to create new stacks, and this is likely to cause a bottleneck in provisioning cloud infrastructure&amp;mdash;one of the very issues that infrastructure as code tools like Pulumi are attempting to solve! (It may be necessary in some cases to restrict this functionality, such as in certain highly regulated industries or verticals.) Preventing users from creating their own stacks would also prevent the use of per-developer stacks like Zephyr uses, which as you&amp;rsquo;ve seen can be enormously helpful in accelerating development and deployment velocity.&lt;/p&gt;
&lt;h2 id="inspecting-rbac-implementation"&gt;Inspecting RBAC Implementation&lt;/h2&gt;
&lt;p&gt;Zephyr&amp;rsquo;s RBAC implementation in Pulumi Cloud is represented in part by a diagram that you also saw earlier in this post:&lt;/p&gt;
&lt;p&gt;&lt;img src="security-stacks-teams.jpg" alt="A diagram illustrating how Zephyr set up role-based access control to their Pulumi stacks"&gt;&lt;/p&gt;
&lt;p&gt;Zephyr set the organization default stack permission to Read. This enabled teams to read all stacks, and enabled the continued use of stack references without any additional configuration. Teams were created to grant &amp;ldquo;Stack editor&amp;rdquo; (also called &amp;ldquo;Stack write access&amp;rdquo;) to stacks based on the overall organizational structure of the company. Thus, the Platform team gained access to the non-production &lt;code&gt;zephyr-infra&lt;/code&gt; and &lt;code&gt;zephyr-k8s&lt;/code&gt; stacks, while the Application team was given access to the non-production &lt;code&gt;zephyr-app&lt;/code&gt; stacks. However, only members of the Production team were given access to the production stacks in all three projects, ensuring that access to update those stacks&amp;mdash;and thus the actual resources running Zephyr&amp;rsquo;s online store&amp;mdash;remained relatively controlled.&lt;/p&gt;
&lt;h2 id="summarizing-best-practices-rbac--security"&gt;Summarizing Best Practices: RBAC &amp;amp; Security&lt;/h2&gt;
&lt;p&gt;The post covered the following guidelines for securing your stacks in Pulumi Cloud:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use Pulumi Cloud&amp;rsquo;s RBAC functionality by setting your organization&amp;rsquo;s default stack permissions to Read or None.&lt;/strong&gt; This is generally the safest way to implement the principle of least privilege, unless you have a good use case for allowing all organization members full write access to all stacks.&lt;/li&gt;
&lt;li&gt;When assigning team permissions, &lt;strong&gt;grant the minimum level of access necessary.&lt;/strong&gt; If a user isn&amp;rsquo;t responsible for making updates to a stack, then give that user stack reader permission through a team membership (remember that you can&amp;rsquo;t assign stack permissions directly to organization members, you have to use a team). Keep in mind, though: if the organization level stack permission has been set to Read, then members already have the equivalent of stack reader permissions on all stacks, so creating a team to grant stack reader permission is unnecessary and superfluous.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Declaratively manage your Pulumi Cloud RBAC configuration&lt;/strong&gt; where possible using the Pulumi Cloud provider from within a Pulumi program.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the &lt;code&gt;--teams&lt;/code&gt; flag with &lt;code&gt;pulumi stack init&lt;/code&gt; to ensure that teams have stack permissions upon stack creation.&lt;/strong&gt; You can also use a team access token to accomplish the same result.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow users to create their own stacks.&lt;/strong&gt; Generally, turning this functionality off (found in the Settings &amp;gt; Access Management screen of Pulumi Cloud) will end up creating a bottleneck, as only organization admins would be allowed to create new stacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next IaC Best Practices post, the focus is on &lt;a href="https://www.pulumi.com/automation/"&gt;Automation API&lt;/a&gt;. Stay tuned to see how Zephyr uses Automation API to further streamline and automate the deployment of their cloud resources!&lt;/p&gt;</description><author>Scott Lowe</author><category>best-practices</category><category>rbac</category><category>security</category></item><item><title>IaC Best Practices: Applying Stack References</title><link>https://www.pulumi.com/blog/iac-best-practices-applying-stack-references/</link><pubDate>Fri, 31 Mar 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-applying-stack-references/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-applying-stack-references/index.png" /&gt;
&lt;p&gt;This is the fourth post in a series of blog posts focused on Zephyr Archaeotech Emporium&amp;mdash;our fictional company&amp;mdash;and their use of Pulumi to manage their online retail store. In the first three posts, you saw how Zephyr&amp;rsquo;s initial use of Pulumi changed as the company grew, and how the use of short-lived per-developer stacks helped Zephyr&amp;rsquo;s application development team meet the demands of a fast-growing company. This post is a complement to the earlier post on structuring Pulumi projects, discussing how Zephyr uses &lt;a href="https://www.pulumi.com/tutorials/building-with-pulumi/stack-references/"&gt;Stack References&lt;/a&gt; to link their projects together and sharing some IaC best practices around the use of Stack References.&lt;/p&gt;
&lt;p&gt;As you may have read in earlier Zephyr posts, the ultimate goal of the Zephyr series is to share best practices on the use of Pulumi to manage your infrastructure and application resources (using a fictional company and a somewhat complex containerized application as the use case). The series exposes those practices over time&amp;mdash;not all right away, and not without also discussing the context for the recommendations. This is deliberate, demonstrating how &amp;ldquo;point-in-time&amp;rdquo; recommendations change based on the needs of the company and its requirements.&lt;/p&gt;
&lt;h2 id="reviewing-zephyrs-current-status"&gt;Reviewing Zephyr&amp;rsquo;s Current Status&lt;/h2&gt;
&lt;p&gt;This post continues the previous post in the series. Zephyr&amp;rsquo;s infrastructure footprint and Pulumi codebase is the same; that is, Zephyr now has &lt;em&gt;three&lt;/em&gt; Pulumi projects (zephyr-infra, zephyr-k8s, and zephyr-app). Each of these projects handles a different aspect of Zephyr&amp;rsquo;s online store:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &amp;ldquo;zephyr-infra&amp;rdquo; project handles base infrastructure (VPC and related constructs).&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;zephyr-k8s&amp;rdquo; project handles the Kubernetes platform layer, building atop the infrastructure components provided by the &amp;ldquo;zephyr-infra&amp;rdquo; project.&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;zephyr-app&amp;rdquo; project handles the deployment of the online store services onto Kubernetes, layering on top of the platform provided by the &amp;ldquo;zephyr-k8s&amp;rdquo; project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reasons for this structure are explained in &lt;a href="https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/"&gt;structuring Pulumi projects&lt;/a&gt; post. For Zephyr, it came down to supporting multiple teams and allowing the application to evolve independently of the infrastructure that supports it.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;You can continue to use the &lt;code&gt;blog/multi-project&lt;/code&gt; branch in the GitHub repositories to see the state of Zephyr&amp;rsquo;s code and projects for this blog post.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;It&amp;rsquo;s clear that there are dependencies across projects: The &lt;code&gt;zephyr-k8s&lt;/code&gt; project needs to know the VPC ID and the subnet IDs from the &lt;code&gt;zephyr-infra&lt;/code&gt; project. Similarly, the &lt;code&gt;zephyr-app&lt;/code&gt; project needs to have access to the &lt;a href="https://www.pulumi.com/templates/kubernetes/"&gt;Kubernetes cluster&lt;/a&gt; details from the &lt;code&gt;zephyr-k8s&lt;/code&gt; project. As the Zephyr team went about building the code that&amp;rsquo;s now in use in their multi-project architecture, they needed to decide how best to handle these cross-project dependencies. The Zephyr team knew that hard-coding output values from one project as configuration values in another stack wasn&amp;rsquo;t the ideal way (and in fact Pulumi &lt;strong&gt;strongly&lt;/strong&gt; recommends against hard-coding output values), but what was the best solution to use? The answer to that question is &lt;em&gt;stack references&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="linking-stacks-with-stack-references"&gt;Linking Stacks with Stack References&lt;/h2&gt;
&lt;p&gt;Stack references allow you to access output values from one stack in another stack. In other words, stack references offer a way to programmatically &amp;ldquo;link&amp;rdquo; stacks in different projects for the purpose of sharing information. You can find more details &lt;a href="https://www.pulumi.com/docs/concepts/stack/#stackreferences"&gt;in the documentation on stack references&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using stack references, the Zephyr team can make the necessary connections they need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The code handling the Kubernetes cluster can reference the VPC ID and subnet IDs from the base infrastructure layer.&lt;/li&gt;
&lt;li&gt;Similarly, the code that is responsible for deploying the online store services can reference the necessary Kubernetes connection details (the Kubeconfig) for the Kubernetes cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see how the Zephyr team did this for their projects, including code samples, toward the end of this post (in the section titled &amp;ldquo;Examining Zephyr&amp;rsquo;s use of stack references&amp;rdquo;). This graphical representation also illustrates Zephyr&amp;rsquo;s current repository and project structure as well as the stack references:&lt;/p&gt;
&lt;p&gt;&lt;img src="gh-repos-pulumi-projects-stacks.jpg" alt="Graphic showing the relationships between three Git repositories and three Pulumi projects"&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s worthwhile to note that any information that needs to be accessible from another stack via a stack reference must be exported as a stack output in the source stack. If you don&amp;rsquo;t mark it as a stack output, then it can&amp;rsquo;t be used in a stack reference. Adding stack outputs after the fact requires little effort and has no impact on existing infrastructure.&lt;/p&gt;
&lt;p&gt;Also, any value retrieved via a stack reference is treated as &lt;a href="https://www.pulumi.com/docs/concepts/inputs-outputs/"&gt;a Pulumi Output&lt;/a&gt;, and therefore may require some extra work to transform values (such as the use of &lt;code&gt;Output.apply&lt;/code&gt;). The recent addition of &lt;code&gt;OutputDetails&lt;/code&gt; support in Pulumi&amp;mdash;you can read more about &lt;code&gt;OutputDetails&lt;/code&gt; &lt;a href="https://www.pulumi.com/blog/stack-reference-output-details/"&gt;in this blog post announcing the functionality&lt;/a&gt;&amp;mdash;helps considerably in this situation. Some SDKs also have language-specific mechanisms that can help; for example, using Go&amp;rsquo;s &lt;code&gt;.AsStringArrayOutput()&lt;/code&gt; method on a &lt;code&gt;StackReference.GetOutput&lt;/code&gt; statement makes referencing subnet IDs from other project much easier.&lt;/p&gt;
&lt;p&gt;While stack references are conceptually straightforward and not difficult to implement, there are some best practices to be mindful of regarding the use of stack outputs and stack references:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When it comes to stack outputs, &lt;strong&gt;export what is needed.&lt;/strong&gt; If it needs to be accessed from outside the stack, export it; otherwise, don&amp;rsquo;t. Why? Stack outputs form the application programming interface (API) by which other Pulumi programs interact with this Pulumi program. Like in any API implementation, expose what the user cares about or is likely to need, and leave the rest as an implementation detail. You can add more stack outputs after the fact with only minimal code changes, and as has been mentioned already this is done with a quick &lt;code&gt;pulumi up&lt;/code&gt; that has no affect on existing infrastructure (you&amp;rsquo;re modifying the stack object itself).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be judicious in your use of stack references.&lt;/strong&gt; This is sort of the &amp;ldquo;flip side&amp;rdquo; of the previous bullet. If you need to reference an output from another stack, then do so; otherwise, don&amp;rsquo;t. If you find yourself needing lots of stack references, then the way in which you&amp;rsquo;ve structured your projects may need to be re-examined so as to minimize the coupling between projects. Additionally, there is a potential &amp;ldquo;speed of light&amp;rdquo; concern here; if where you are executing your Pulumi code is across a high-latency link from where you store your Pulumi state, stack references will be affected by that latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you need to expose lots of outputs on a stack, consider exporting a structured data object.&lt;/strong&gt; It&amp;rsquo;s possible to construct a JSON object (or dict or struct, depending on your language) to hold all the stack outputs, and then export that object. Then a stack reference can read that object, resulting in a single call to the backend. Be aware, though, that you&amp;rsquo;ll need to write the necessary code to understand/import/unmarshall that JSON object in the referring stack, so there is a small amount of extra work required in this situation.&lt;/li&gt;
&lt;li&gt;In line with parameterizing as much of your code as possible, &lt;strong&gt;also be sure to parameterize your stack references.&lt;/strong&gt; A stack reference is built using an organization name, a project name, and a stack name. Don&amp;rsquo;t hardcode these values; instead, use configuration values to allow the users to specify from which source stack(s) the values will be referenced. You&amp;rsquo;ll see an example of that in the next section.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for handling cascading stack updates.&lt;/strong&gt; If a stack has outputs being consumed via stack references, what happens when those stack outputs change? The Pulumi Service &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/webhooks/"&gt;supports webhooks&lt;/a&gt; that can help, and Pulumi is currently exploring options for having Pulumi Deployments help with this (see &lt;a href="https://github.com/pulumi/pulumi-cloud-requests/issues/181"&gt;Pulumi Cloud Requests GitHub issue&lt;/a&gt;). In practice this should be uncommon, and frequent occurrences may be a good indicator that your resource partitioning (how resources are split among multiple projects) needs to be adjusted.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One thing not listed above is security&amp;mdash;and that&amp;rsquo;s because a later post in the series focuses on this topic. You&amp;rsquo;ll want to keep the security of your stacks in mind when planning for the use of stack references. The teams managing the stacks where the stack references exist will need at least read-only access to the stacks where the outputs exist. (For more on this front, refer to &lt;a href="https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/"&gt;the IaC Best Practices post on RBAC and security&lt;/a&gt;.)&lt;/p&gt;
&lt;h2 id="examining-use-of-stack-references"&gt;Examining Use of Stack References&lt;/h2&gt;
&lt;p&gt;With these best practices in mind, you can examine the Zephyr team&amp;rsquo;s implementation to see how they put these recommendations into action.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In the code for the base infrastructure stack, you can see that Zephyr &lt;a href="https://github.com/pulumi/zephyr-infra/blob/blog/multi-project/index.ts#L14-L17"&gt;exported the essential values&lt;/a&gt; needed by the Kubernetes platform stack:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Export some values for use elsewhere
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpcId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;eksVpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpcId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privSubnetIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;eksVpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateSubnetIds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pubSubnetIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;eksVpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicSubnetIds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the Kubernetes platform stack, the Zephyr team &lt;a href="https://github.com/pulumi/zephyr-k8s/blob/blog/multi-project/index.ts#L10-L12"&gt;parameterized the values&lt;/a&gt; needed for the stack reference. This particular approach, by the way, is key to preserving &lt;a href="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/"&gt;the per-developer stacks&lt;/a&gt; to which the Zephyr team has grown accustomed (each developer needs to specify the correct organization, project, and stack name):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Grab some configuration values
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Some code omitted here
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;infraOrgName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;infraOrgName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;infraProjName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;infraProjName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;infraStackName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;infraStackName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Create a StackReference to get information from base stack
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;infraSr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;infraOrgName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;infraProjName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;infraStackName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As with the base infrastructure stack, &lt;a href="https://github.com/pulumi/zephyr-k8s/blob/blog/multi-project/index.ts#L36-L37"&gt;only the key value needed by other stacks&lt;/a&gt; is exported (in this case, the Kubeconfig required to access this cluster):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Export some values for use elsewhere
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kubeconfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;eksCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kubeconfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, in the application stack, the &lt;a href="https://github.com/pulumi/zephyr-app/blob/blog/multi-project/infra/index.ts#L7-L9"&gt;stack reference values are again parameterized&lt;/a&gt;, and the Kubeconfig&amp;mdash;necessary for the application stack to deploy onto the provisioned Kubernetes cluster&amp;mdash;is &lt;a href="https://github.com/pulumi/zephyr-app/blob/blog/multi-project/infra/index.ts#L11-L13"&gt;referenced via a stack reference&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Grab some configuration values
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k8sOrgName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;k8sOrgName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k8sProjName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;k8sProjName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k8sStackName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;k8sStackName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Create a StackReference to get Kubeconfig from base stack
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kubeSr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k8sOrgName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k8sProjName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k8sStackName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;All of the GitHub links in the paragraphs above reference the &lt;code&gt;blog/multi-project&lt;/code&gt; branch in each repository, which has the code as of this blog post (and the earlier blog posts).&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="summarizing-best-practices-stack-references"&gt;Summarizing Best Practices: Stack References&lt;/h2&gt;
&lt;p&gt;This post covered the following guidelines for working with Pulumi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Export the minimum of what&amp;rsquo;s needed&lt;/strong&gt; from your stacks. It&amp;rsquo;s not difficult to add stack outputs later, and you can do it with no impact to existing infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be judicious in the use of stack references.&lt;/strong&gt; Reducing stack references where possible helps keep projects from becoming overly coupled together, much in the same way as minimizing stack outputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a structured data object for large numbers of values that need to be referenced.&lt;/strong&gt; This enables Pulumi to retrieve all the data with a single call, but be aware you&amp;rsquo;ll need to write extra code to understand the data structure in the referring stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameterize your stack references.&lt;/strong&gt; Don&amp;rsquo;t hardcode organization, project, or stack name values. Instead, pass these in as configuration values (with default values applied, where applicable).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan to build mechanisms to handle cascading stack updates.&lt;/strong&gt; Using the Pulumi Service&amp;rsquo;s webhook functionality can be useful here. Frequent instances of cascading updates might be an indicator that your project structure needs to be revisited for optimal resource partitioning.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next post in the IaC best practices series focuses on security and role-based access control (RBAC)&amp;mdash;a topic you won&amp;rsquo;t want to miss! Before then, the next visit to the &amp;ldquo;Zephyr universe&amp;rdquo; will be to take a look at an oft-overlooked use case for Pulumi: local testing. Come see how to use Pulumi to automate local testing for development!&lt;/p&gt;</description><author>Scott Lowe</author><category>best-practices</category><category>cloud-engineering</category><category>kubernetes</category></item><item><title>IaC Best Practices: Structuring Pulumi Projects</title><link>https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/</link><pubDate>Fri, 17 Mar 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-structuring-pulumi-projects/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-structuring-pulumi-projects/index.png" /&gt;
&lt;p&gt;This is the third post in a series of blog posts focused on Zephyr Archaeotech Emporium&amp;mdash;our fictional company&amp;mdash;and their use of Pulumi to manage their online retail store. In the first post, you saw how Zephyr initially decided to go with a single &lt;a href="https://www.pulumi.com/docs/iac/concepts/projects/"&gt;Pulumi project&lt;/a&gt; for managing deployments of their online retail store application. In this post, you&amp;rsquo;ll see how Zephyr&amp;rsquo;s use of Pulumi changes as their company grows and evolves.&lt;/p&gt;
&lt;p&gt;The first post about &lt;a href="https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/"&gt;organizing code and stacks&lt;/a&gt; specifically mentions that the series will show &amp;ldquo;how Zephyr&amp;rsquo;s use of Pulumi evolves as the company grows and their online retail store application changes to accommodate their growth.&amp;rdquo; While the ultimate goal of this series is to discuss best practices for using Pulumi to manage a fairly complex containerized application, it&amp;rsquo;s important to note that many best practices are &amp;ldquo;point in time&amp;rdquo; recommendations: best practices are context-dependent and the recommended practices for a given organization will change as the company and its infrastructure needs also change. This blog post is an example of that evolution in action: the practices that worked well for Zephyr while they were smaller and had smaller infrastructure and less teams working on that infrastructure are no longer ideal as the company has grown.&lt;/p&gt;
&lt;h2 id="growing-development-teams"&gt;Growing Development Teams&lt;/h2&gt;
&lt;p&gt;As a result of launching its online presence, Zephyr saw amazing success. The market for arcane artifacts and novel curiosities is booming, and Zephyr is becoming a leader in this market. Funded by this success, Zephyr has grown rapidly. Along with this growth, Zephyr&amp;rsquo;s software engineering and IT teams also grew and expanded. Fortunately, Zephyr&amp;rsquo;s &lt;a href="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/"&gt;use of per-developer stacks with short-lived ephemeral Git branches&lt;/a&gt; positioned the development teams well for this growth.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;For a look at the state of Zephyr&amp;rsquo;s Pulumi code as it stood as of the end of the second post in the series, see the &lt;a href="https://github.com/pulumi/zephyr-app/tree/blog/dev-stacks"&gt;&lt;code&gt;blog/dev-stacks&lt;/code&gt;&lt;/a&gt; branch of the &lt;a href="https://github.com/pulumi/zephyr-app"&gt;&lt;code&gt;pulumi/zephyr-app&lt;/code&gt;&lt;/a&gt; repository on GitHub.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Naturally this growth has resulted in some separation of duties. The IT team now has responsibility for the infrastructure that supports the online retail store. Within that team, some team members are focused on &amp;ldquo;core&amp;rdquo; infrastructure, while other team members are focused only on &lt;a href="https://kubernetes.io/docs/concepts/security/multi-tenancy/"&gt;Kubernetes&lt;/a&gt; (a natural decision given Zephyr&amp;rsquo;s use of Kubernetes in their architecture). The application team continues to remain responsible for the online store application itself.&lt;/p&gt;
&lt;p&gt;This left Zephyr again asking some questions about the best path forward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Should they use a single project, or multiple projects?&lt;/strong&gt; Zephyr had used a single Pulumi project up until now, but growth and organizational change meant they wanted to make sure they were using Pulumi in a way that best reflected their needs. What are the advantages and disadvantages of each approach?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If multiple projects, how many projects?&lt;/strong&gt; What&amp;rsquo;s the best way to determine how many projects are needed? What factors play a role in this decision?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="structuring-pulumi-projects"&gt;Structuring Pulumi Projects&lt;/h2&gt;
&lt;p&gt;One project, or multiple projects? And if multiple projects, how many projects? These are the core questions the Zephyr needed to resolve. In the end, there are a number of key factors that determine the answers to these questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Zephyr has only a single application (so far!), but in situations where there are multiple applications then keeping those applications (and sometimes their associated infrastructure, depending on other factors) in separate Pulumi projects is generally recommended. In some ways, &amp;ldquo;use case&amp;rdquo; is an aggregation of other factors&amp;mdash;a new application with a distinct set of resources that do not share a lifecycle with existing resources and are potentially managed by a different team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Company/department/team structure:&lt;/strong&gt; How a company/department/team is organized should certainly be taken into account when structuring your Pulumi projects. In Zephyr&amp;rsquo;s early days with Pulumi, a single team managed everything so a single project was fine. Now with multiple teams, Zephyr will likely find using multiple projects to be more effective.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security:&lt;/strong&gt; Closely related to the previous bullet, security is a factor that affects how projects and stacks should be structured. In Zephyr&amp;rsquo;s case, the ability for one team to affect another team&amp;rsquo;s resources was present when everything was a single project. With multiple projects, Zephyr can use stack permissions and role-based access control (RBAC) in Pulumi Cloud to appropriately control &lt;em&gt;who&lt;/em&gt; can affect &lt;em&gt;what&lt;/em&gt;. (The IaC Best Practices blog post on &lt;a href="https://www.pulumi.com/blog/iac-best-practices-implementing-rbac-and-security/"&gt;RBAC and security&lt;/a&gt; discusses this topic in more detail.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The relationship between resources:&lt;/strong&gt; The relationships between resources&amp;mdash;such as which resources are dependent upon other resources&amp;mdash;is another factor that feeds into your Pulumi project structure. A &amp;ldquo;shared infrastructure layer&amp;rdquo; is often best handled in a separate project of its own, but may pull in resources that might otherwise have been kept with an application in its own project. (In some respects, this is a &amp;ldquo;counter-balance&amp;rdquo; to the first bullet point.) On the other hand, resources that belong exclusively to a specific application or function might be best bundled in the same Pulumi project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource lifecycle:&lt;/strong&gt; Do resources have a shared lifecycle, meaning they are always created, updated, or deleted together? If so, then those resources should be placed in the same project. If the resources do have a shared lifecycle, then placing them in the same project simplifies lifecycle management. A great example of resources with a shared lifecycle would be a Kubernetes Deployment and the associated Kubernetes Service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change rate of resources:&lt;/strong&gt; Some resources may change very infrequently. Consider the example of an &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html"&gt;AWS VPC&lt;/a&gt; and associated objects. Once it is created, it may not change for extended periods of time, if ever. On the other hand, some resources may change frequently (in other words, these resources have a high change rate). A fast-moving software development team may revise the Kubernetes Deployment regularly to pull in a new container image with updated code. In a single project, combining resources that have a high change rate with resources that have a low change rate may be suboptimal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blast radius:&lt;/strong&gt; Finally, there is the consideration of what happens in the event of human error, and limiting the &amp;ldquo;blast radius.&amp;rdquo; With a single project, &lt;em&gt;all&lt;/em&gt; the resources in that project are within the blast radius&amp;mdash;all of them could potentially be affected in the event of human error. Using separate Pulumi projects can limit the blast radius. It&amp;rsquo;s not something that should drive the decision all by itself, but it &lt;em&gt;is&lt;/em&gt; a factor to be considered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that you have an idea of some of the factors that affect how users structure their Pulumi projects, what are the advantages and disadvantages of each approach?&lt;/p&gt;
&lt;p&gt;For single projects, the key advantage is &lt;em&gt;simplicity.&lt;/em&gt; There&amp;rsquo;s no denying that a single project is simpler and less complex than working with multiple projects. This advantage, though, is also the key disadvantage. A single project does not afford the flexibility to let different teams manage different aspects of the infrastructure, for example, nor does it offer the ability to split resources that don&amp;rsquo;t have a shared lifecycle. And, as mentioned earlier, blast radius is a concern.&lt;/p&gt;
&lt;p&gt;For multiple projects, the key advantage is &lt;em&gt;flexibility.&lt;/em&gt; It&amp;rsquo;s true that the added flexibility comes at the cost of some added complexity (a future post will explore one aspect of that added complexity in the form of StackReferences). However, you gain the ability to separate infrastructure resources according to team ownership/responsibility, relationship to other resources (i.e., grouping resources in a &amp;ldquo;shared infrastructure layer&amp;rdquo;), or any of the other dimensions mentioned above.&lt;/p&gt;
&lt;p&gt;In some ways, the decision points for single vs. multiple projects are analogous to the decision points for monolith vs. microservice: independent scaling and flexibility versus simplicity.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In general, most customers end up using multiple Pulumi projects per use case.&lt;/em&gt; As you&amp;rsquo;ve seen, though, there are a number of factors that influence that decision and will affect how many projects end up in use.&lt;/p&gt;
&lt;h2 id="moving-forward-with-projects"&gt;Moving Forward with Projects&lt;/h2&gt;
&lt;p&gt;Zephyr had a number of factors in play, not the least of which was the organizational change resulting from their growth and success. In the end, Zephyr decided to restructure their use of Pulumi for the online retail store into three separate projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An infrastructure project: This project handled the AWS VPC and related infrastructure resources.&lt;/li&gt;
&lt;li&gt;A platform project: This project managed the Kubernetes cluster(s).&lt;/li&gt;
&lt;li&gt;An application project: This project handled the actual online retail store itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;You can use a combination of code refactoring and &lt;code&gt;pulumi import&lt;/code&gt; to assist in the process of restructuring Pulumi projects, but it some cases it may be easier to create new infrastructure and migrate over to it.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the &lt;a href="https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/"&gt;code organization and stacks&lt;/a&gt; post, you read about some of the options for where to store Pulumi code relative to the applications it supports. At that time, Zephyr had decided to go with what&amp;rsquo;s called a monorepo approach, meaning that the Pulumi code was stored in the same repository as the application code. In conjunction with the project reorganization, Zephyr also decided to change how their code was organized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code for the infrastructure project moved to a separate &amp;ldquo;zephyr-infra&amp;rdquo; repository&lt;/li&gt;
&lt;li&gt;A new &amp;ldquo;zephyr-k8s&amp;rdquo; repository housed the code for the platform project&lt;/li&gt;
&lt;li&gt;The code for the online retail store application itself remained in the same &amp;ldquo;zephyr-app&amp;rdquo; repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that relocating code (to a different GitHub repository or different filesystem location within a repository) generally has no impact on Pulumi. As long as the project name and stack name do not change, then changing the filesystem location won&amp;rsquo;t affect anything&amp;mdash;keeping in mind, of course, that filesystem paths and such may need to be adjusted in code to account for any such changes.&lt;/p&gt;
&lt;p&gt;After restructuring their projects, Zephyr&amp;rsquo;s arrangement of Pulumi projects and stacks, along with their associated GitHub repositories, looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="structuring-projects-stacks.jpg" alt="A diagram showing three Pulumi projects and stacks along with associated GitHub repositories"&gt;&lt;/p&gt;
&lt;p&gt;These repositories are available for you to review on GitHub (&lt;a href="https://github.com/pulumi/zephyr-infra"&gt;zephyr-infra&lt;/a&gt;, &lt;a href="https://github.com/pulumi/zephyr-k8s"&gt;zephyr-k8s&lt;/a&gt;, and &lt;a href="https://github.com/pulumi/zephyr-app"&gt;zephyr-app&lt;/a&gt;); use the branch/tag selector to find the &lt;code&gt;blog/multi-project&lt;/code&gt; branch in each repository to see the state of the code as of this blog post. Be aware that the process for standing up the Zephyr online store is now a bit more complex; check the instructions in each repository for more details if you&amp;rsquo;d like to give these projects a spin.&lt;/p&gt;
&lt;h2 id="summarizing-best-practices-pulumi-project-structures"&gt;Summarizing Best Practices: Pulumi Project Structures&lt;/h2&gt;
&lt;p&gt;This post covered the following recommended practices for working with Pulumi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use multiple Pulumi projects&lt;/strong&gt; when you need the added flexibility that multiple Pulumi projects offer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Align your Pulumi projects&lt;/strong&gt; according to external factors like company/department/team structure, resource dependencies, lifecycle, and use case/application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change the filesystem location&lt;/strong&gt; for Pulumi code as needed without impacting the resources managed by that code, as long as the Pulumi project name and stack name don&amp;rsquo;t change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next post will focus on some specific technical details related to Zephyr&amp;rsquo;s project reorganization; specifically, how they used StackReferences to share information between their Pulumi projects. Stay tuned!&lt;/p&gt;</description><author>Christian Nunciato</author><author>Scott Lowe</author><category>best-practices</category><category>cloud-engineering</category></item><item><title>IaC Best Practices: Enabling Developer Stacks &amp; Git Branches</title><link>https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/</link><pubDate>Fri, 10 Mar 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-enabling-developer-stacks-git-branches/index.png" /&gt;
&lt;p&gt;In the first post about &lt;a href="https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/"&gt;code organization and stacks&lt;/a&gt;, we introduced Zephyr, a fictional company that uses Pulumi to manage its online retail store. Following on from that post, which discusses code organization and stacks, this post explores two more questions users frequently ask when working with Pulumi in teams &amp;mdash; namely, How can I best enable multiple developers to collaborate on a Pulumi project? And how can I use Git and Git branching to support this kind of collaboration? In this post, we&amp;rsquo;ll provide some guidance and best practices around these topics, using Zephyr and its online store as the use case.&lt;/p&gt;
&lt;p&gt;The ultimate goal of this series is to discuss best practices for using Pulumi to manage a fairly complex containerized application. However, it&amp;rsquo;s important to note that these practices will emerge over the course of the series &amp;mdash; not all immediately, and not all in the beginning. This is a deliberate decision to allow you to see how Zephyr&amp;rsquo;s use of Pulumi evolves as the company grows and its retail application changes to accommodate its growth.&lt;/p&gt;
&lt;h2 id="existing-iac-workflow"&gt;Existing IaC Workflow&lt;/h2&gt;
&lt;p&gt;When we last met up with the Zephyr team, they were off and running, managing their newly refactored online store, Zephyr Archaeotech Emporium, with a single &lt;a href="https://www.pulumi.com/docs/concepts/projects/"&gt;Pulumi project&lt;/a&gt; and two &lt;a href="https://www.pulumi.com/docs/concepts/stack/"&gt;Pulumi stacks&lt;/a&gt; &amp;mdash; one for development (&lt;code&gt;dev&lt;/code&gt;) and another for production (&lt;code&gt;prod&lt;/code&gt;). The team had chosen to use one Git repository (a monorepo) to manage the code for the online store and its infrastructure after refactoring the store into a set of containerized microservices deployed with Kubernetes on &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html"&gt;Amazon EKS&lt;/a&gt;.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;For a snapshot of the code as it was at the end of the first post in the series, see the &lt;a href="https://github.com/pulumi/zephyr-app/tree/blog/original"&gt;&lt;code&gt;blog/original&lt;/code&gt;&lt;/a&gt; branch of the &lt;a href="https://github.com/pulumi/zephyr-app"&gt;&lt;code&gt;pulumi/zepyhr-app&lt;/code&gt;&lt;/a&gt; repository on GitHub.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Up to now, the two-stack approach has worked well for the team. It&amp;rsquo;s allowed them to focus on building new features, integrating those features into the shared &lt;code&gt;dev&lt;/code&gt; environment by developing and testing locally (which we&amp;rsquo;ll cover in a future post) and merging their &lt;a href="https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow"&gt;feature branches&lt;/a&gt; regularly into the base branch, &lt;code&gt;main&lt;/code&gt;, using standard GitHub pull-request workflows. Around once a day, as the team&amp;rsquo;s changes are merged into &lt;code&gt;main&lt;/code&gt;, the team lead (and original author of the Pulumi program) deploys them into the &lt;code&gt;dev&lt;/code&gt; environment with Pulumi with the following commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git pull origin main
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; infra
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack &lt;span class="k"&gt;select&lt;/span&gt; zephyr/dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This builds new container images for any services whose code has changed since the last stack update, pushes those images to &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html"&gt;Amazon ECR&lt;/a&gt;, and updates the EKS cluster to deploy the services automatically.&lt;/p&gt;
&lt;p&gt;Then, around once a week, as new features are ready for customers, the team lead deploys the &lt;code&gt;main&lt;/code&gt; branch of the repository into production similarly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git pull origin main
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; infra
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack &lt;span class="k"&gt;select&lt;/span&gt; zephyr/prod
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The deployment process is still a bit more manual than the team would like &amp;mdash; but since updates typically take only a few minutes, the team lead is generally satisfied with the status quo.&lt;/p&gt;
&lt;p&gt;A few more details worth noting out about Team Zephyr&amp;rsquo;s current setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Both of the project&amp;rsquo;s stack-configuration files, &lt;code&gt;Pulumi.dev.yaml&lt;/code&gt; and &lt;code&gt;Pulumi.prod.yaml&lt;/code&gt;, are checked into version control. This is safe and considered good practice (even with &lt;a href="https://www.pulumi.com/docs/concepts/secrets/"&gt;secrets&lt;/a&gt;) because it allows the team to review or change the configuration of a given stack easily by viewing or editing the code in each file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;main&lt;/code&gt; branch is the only long-lived branch in the repository; all others (e.g., developers&amp;rsquo; feature branches) are deleted once they&amp;rsquo;re merged into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Thanks to this workflow, the application deployed into the &lt;code&gt;dev&lt;/code&gt; environment tracks closely with the latest code in the &lt;code&gt;main&lt;/code&gt; branch. As such, the &lt;code&gt;dev&lt;/code&gt; stack serves as a kind of pre-prod or integration environment that not only allows the team to dogfood new features as they&amp;rsquo;re added, but also to catch bugs or other regressions before they escape into production. When things do go wrong, the team can review the list of recent stack updates to identify the source of the defect and get it fixed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zephyr is using the Pulumi Service backend with a single organization (that everyone on the team belongs to) and separate AWS accounts for the &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt; environments. Everyone on the team has read/write access to the AWS account used for development, but only the team lead has access to the AWS account used for production.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="growing-pains"&gt;Growing Pains&lt;/h2&gt;
&lt;p&gt;Life for Team Zephyr was good &amp;mdash; for a while.&lt;/p&gt;
&lt;p&gt;Then one day during a particularly active sprint, a developer who&amp;rsquo;d started working on an improvement to the checkout process realized it might benefit from an updated version of Redis, which appeared to be presently &lt;a href="https://github.com/pulumi/zephyr-app/blob/e61b5b02b0eebe14782fba8ee0343952353c9fd0/infra/index.ts#L1045"&gt;pinned at 6.0 in the Pulumi program&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutRedisDeployment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;checkout-redis-deployment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;containers&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;redis:6.0-alpine&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A quick glance at the &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt; stacks in the Pulumi Service confirmed this was the case.&lt;/p&gt;
&lt;p&gt;After a quick spike to validate the enhancement locally, the developer felt confident that a Redis upgrade was the way to go &amp;mdash; but since the deployed version of Redis would ultimately dictate the code that&amp;rsquo;d have to be written to finish the feature for real, they couldn&amp;rsquo;t really be sure without testing the theory with a full deployment. What they really wanted was to be able to deploy a Redis upgrade, along with the associated code changes, into an AWS environment as much like production as possible &amp;mdash; the &lt;code&gt;dev&lt;/code&gt; environment, say &amp;mdash; without having to go through the process of making a pull request, getting it merged, and waiting for the team lead to deploy the &lt;code&gt;dev&lt;/code&gt; stack only to realize later that the change didn&amp;rsquo;t actually work as expected, and be forced to revert.&lt;/p&gt;
&lt;p&gt;In situations like these, it can be tempting to suggest that the developer simply pick up and use the &lt;code&gt;dev&lt;/code&gt; stack &amp;mdash; especially on small, fast-moving teams like Zephyr&amp;rsquo;s where everyone already has access to the Pulumi stack and the shared cloud environment. Indeed, Pulumi makes this sort of thing easy: to deploy their local changes into the &lt;code&gt;dev&lt;/code&gt; environment, our developer need only check out their feature branch, select the &lt;code&gt;dev&lt;/code&gt; stack, run &lt;code&gt;pulumi up&lt;/code&gt;, and then wait a few moments for the changes to be deployed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; infra
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack &lt;span class="k"&gt;select&lt;/span&gt; zephyr/dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach definitely works &amp;mdash; but it presents at least a few problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The team would lose all of the practical benefits of having the &lt;code&gt;dev&lt;/code&gt; stack track closely with the latest content in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The team lead&amp;rsquo;s deployment of &lt;code&gt;main&lt;/code&gt; into &lt;code&gt;dev&lt;/code&gt; later that day would overwrite the developer&amp;rsquo;s own feature-branch deployment. There&amp;rsquo;s a good chance the lead would notice the diff of the deployment preview and deduce that someone else on the team had possibly deployed into &lt;code&gt;dev&lt;/code&gt; out of band:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; kubernetes:apps/v1:Deployment (create):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; spec: {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; template: {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; spec: {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; containers: [
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; [0]: {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- image: &amp;#34;redis:6.0-alpine&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ image: &amp;#34;redis:7.0-alpine&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But if they didn&amp;rsquo;t, the developer&amp;rsquo;s ad-hoc deployment would be lost. At best, it&amp;rsquo;d force both team members to drop what they were doing and get in touch to figure out what to do next. At worst, it could lead to confusion and the potential for shipping untested code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In addition to raising the risk of developers overwriting each other&amp;rsquo;s work, active collaboration the same stack increases the likelihood of deployment conflicts. On the one hand, this is a good thing in that Pulumi prevents same-stack updates from running concurrently:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ pulumi up
error: [409] Conflict: Another update is currently in progress.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But on the other, it&amp;rsquo;s a not-so-good thing in that it&amp;rsquo;s a reduction in development velocity: The update that can&amp;rsquo;t run because another update is in progress is an update that will have to wait until sometime later. The more often this happens, the longer it ultimately takes for the team to get the validation it needs to keep moving forward.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So facing these issues, what should the Zephyr team do? The &lt;code&gt;dev&lt;/code&gt; stack had originally been created for rapid development &amp;mdash; and that worked well, when there was only one developer working on infrastructure. With several, however, things clearly get complicated. Should the team be using a &lt;code&gt;dev&lt;/code&gt; stack at all? If so, how? And if not, what should they be doing instead?&lt;/p&gt;
&lt;h2 id="stepping-back-to-reassess"&gt;Stepping Back to Reassess&lt;/h2&gt;
&lt;p&gt;With what we&amp;rsquo;ve learned so far, we can return to the questions we raised at the beginning of this post and begin to sketch out a few recommendations for Team Zephyr. Those questions, again, were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How can I best enable multiple developers to collaborate on a Pulumi project?&lt;/strong&gt; For Zephyr, an ideal answer to this question would give every member of the team the ability to iterate on both application and infrastructure code concurrently, deploying as often as needed without having to worry about disrupting the work or the velocity of anyone else on the team.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How can I use Git and Git branching to support this collaboration?&lt;/strong&gt; So far, the feature-branch/pull-request workflow has been serving the Zephyr team well. Should they continue on the same path, merging frequently to &lt;code&gt;main&lt;/code&gt; and deploying to &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt; from there? Or is there some alternative workflow that might serve the team better, either now or at some time in the future?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s answer these questions now by taking a look at how the Zephyr team handled them.&lt;/p&gt;
&lt;h3 id="using-developer-stacks"&gt;Using Developer Stacks&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve seen a few ways that having multiple developers share a single stack for ad-hoc deployment can lead to various problems. But we&amp;rsquo;ve also seen at least one situation in which being able to run ad-hoc deployments &amp;mdash; without the risk of colliding with existing environments &amp;mdash; can be quite valuable. In fact there are many reasons you might want to do this &amp;mdash; and fortunately Pulumi makes it quite easy to do so.&lt;/p&gt;
&lt;p&gt;The best practice is to allow developers to create and manage their own Pulumi stacks. In this workflow, developers working on feature branches are able to iterate freely on both application and infrastructure, deploying as often as necessary into a shared cloud environment (typically a sandbox environment, isolated from production) and destroying cloud infrastructure when it&amp;rsquo;s no longer needed.&lt;/p&gt;
&lt;p&gt;For the developer, the typical workflow looks something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check out a new feature branch&lt;/li&gt;
&lt;li&gt;Make changes to application and/or infrastructure code&lt;/li&gt;
&lt;li&gt;Select your personal developer stack with &lt;code&gt;pulumi stack select&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Deploy your stack into your team&amp;rsquo;s shared cloud environment with &lt;code&gt;pulumi up&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Commit your code and submit a pull request, optionally including a link either to the Pulumi update (e.g., when using the Pulumi Service) or to certain key resources belonging to the stack &amp;mdash; for example, an application&amp;rsquo;s primary HTTP endpoint&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The benefits of a workflow like this one are many:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No more resource-naming collisions. Because Pulumi names cloud resources &lt;a href="https://www.pulumi.com/docs/concepts/resources/names/#autonaming"&gt;uniquely by default&lt;/a&gt;, no two stacks will ever produce cloud resources with the same names.&lt;/li&gt;
&lt;li&gt;No more deployment conflicts. When every developer has a stack of their own, they can deploy to the sandboxed environment as often as necessary, without having to wait for another deployment to finish first.&lt;/li&gt;
&lt;li&gt;Everyone on the team has their own production-like environment to use for end-to-end testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://www.pulumi.com/blog/iac-best-practices-enabling-developer-stacks-git-branches/dev-stacks-git-branches-workflow.jpg" alt="An image showing the main branch deploying with test and prod stacks and feature branches deploying with developer stacks"&gt;&lt;/p&gt;
&lt;p&gt;For Zephyr, the quickest path to adopting this workflow was to use the &lt;code&gt;dev&lt;/code&gt; stack as a foundation for new developer stacks. To do that, team members simply ran the following Pulumi commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack init zephyr/developer-name --copy-config-from zephyr/dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Developer stacks are considered ephemeral, so they&amp;rsquo;re torn down with &lt;code&gt;pulumi destroy&lt;/code&gt; when not in use in order to cut down on cost. Stack configuration files (e.g., &lt;code&gt;Pulumi.developer-name.yaml&lt;/code&gt;) are checked into version control and stored alongside other stacks like &lt;code&gt;Pulumi.dev.yaml&lt;/code&gt; and &lt;code&gt;Pulumi.prod.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="keeping-feature-branch-workflows"&gt;Keeping Feature-Branch Workflows&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve also seen that the Zephyr team&amp;rsquo;s existing Git workflow (short-lived feature branches merged frequently into a long-lived base branch) has worked well for them &amp;mdash; and it turns out this workflow also works well when combined with the use of developer stacks.&lt;/p&gt;
&lt;p&gt;For example, after creating and deploying a new stack to validate and test the proposed Redis upgrade, our Zephyr developer simply included a link to the application (deployed with their dev stack) in the associated GitHub pull request. Reviewers were then able to follow the link to the online store to see the effects of the shopping-cart change firsthand, which meant they were able to approve and get the change merged quickly and confidently knowing it&amp;rsquo;d work when deployed into production.&lt;/p&gt;
&lt;h3 id="naming-stacks-clearly"&gt;Naming Stacks Clearly&lt;/h3&gt;
&lt;p&gt;Lastly, having adopted the use of developer stacks, the Zephyr team realized the name &amp;ldquo;dev&amp;rdquo; probably didn&amp;rsquo;t work quite as well as it once had for the &lt;code&gt;dev&lt;/code&gt; stack. Since the stack and its long-running deployment had essentially been serving (and continued to serve) as an environment for testing changes before releasing them into production, the team decided to rename the &lt;code&gt;dev&lt;/code&gt; stack &lt;code&gt;test&lt;/code&gt; instead. To do that, they simply ran:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack &lt;span class="k"&gt;select&lt;/span&gt; zephyr/dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pulumi stack rename zephyr/test
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="viewing-the-next-iteration-of-code"&gt;Viewing the Next Iteration of Code&lt;/h2&gt;
&lt;p&gt;You can view the second iteration of Zephyr&amp;rsquo;s Pulumi and application code &amp;mdash; the iteration that corresponds to the decisions described in this blog post &amp;mdash; by navigating to the &lt;a href="https://github.com/pulumi/zephyr-app/"&gt;Zephyr App GitHub repository&lt;/a&gt;. From the branch/tag dropdown, switch from the &lt;code&gt;main&lt;/code&gt; branch to the &lt;a href="https://github.com/pulumi/zephyr-app/tree/blog/dev-stacks"&gt;&lt;code&gt;blog/dev-stacks&lt;/code&gt;&lt;/a&gt; branch.&lt;/p&gt;
&lt;p&gt;From that GitHub repository, you can also choose to deploy the Pulumi code yourself. Full instructions for deploying the code are found in the repository.&lt;/p&gt;
&lt;h2 id="summarizing-best-practices-developer-stacks-and-git-branches"&gt;Summarizing Best Practices: Developer Stacks and Git Branches&lt;/h2&gt;
&lt;p&gt;In this post, we covered several new guidelines for working collaboratively with Pulumi, all aimed at optimizing for high team and developer velocity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use short-lived feature branches&lt;/strong&gt; and merge them frequently into a single base branch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy the base branch regularly into a long-running pre-prod environment&lt;/strong&gt; to continuously test the integration of the components of the system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable developers to create and use dev stacks of their own&lt;/strong&gt; as sandboxes for development, testing, and collaboration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name your stacks clearly&lt;/strong&gt; and in alignment with the environments they deploy into &amp;mdash; &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next Zephyr blog post will dive into how the Zephyr team structures their Pulumi projects. Stay tuned!&lt;/p&gt;</description><author>Christian Nunciato</author><author>Scott Lowe</author><category>best-practices</category><category>cloud-engineering</category><category>aws</category><category>eks</category></item><item><title>IaC Best Practices: Understanding Code Organization &amp; Stacks</title><link>https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/</link><pubDate>Mon, 20 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/iac-best-practices-understanding-code-organization-stacks/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/iac-best-practices-understanding-code-organization-stacks/index.png" /&gt;
&lt;p&gt;This is the first in a series of blog posts that explores how a fictional company&amp;mdash;Zephyr Archaeotech Emporium&amp;mdash;uses Pulumi to manage their online retail store. This post explores a couple of common questions that users ask when working with Pulumi; specifically, where should I store my Pulumi code? And how do I support multiple environments with Pulumi? This post will provide some guidance and &lt;a href="https://www.pulumi.com/what-is/what-is-infrastructure-as-code/"&gt;Infrastructure as Code&lt;/a&gt; best practices around these topics, using Zephyr and their online store as the use case.&lt;/p&gt;
&lt;p&gt;The ultimate goal of this series is to discuss recommended practices for using Pulumi to manage a fairly complex containerized application. However, it&amp;rsquo;s important to note that these practices will emerge over the course of the series&amp;mdash;not all immediately, and not all in the beginning. This is a deliberate decision to allow you to see how Zephyr&amp;rsquo;s use of Pulumi evolves as the company grows and their online retail store application changes to accommodate their growth.&lt;/p&gt;
&lt;h2 id="setting-up-the-scenario"&gt;Setting Up the Scenario&lt;/h2&gt;
&lt;p&gt;Zephyr is short for Zephyr Archaeotech Emporium, the fictional company in our scenario. Zephyr is an online retailer that specializes in the sale of &amp;ldquo;rare arcane artifacts and replicas.&amp;rdquo; Over the past few years, the company has experienced a significant increase in its online presence, making it easier for customers to purchase unique and mysterious items. Zephyr&amp;rsquo;s collection includes a variety of rare and unusual objects that are difficult to find elsewhere, making it a popular destination for collectors, enthusiasts, and adventurers.&lt;/p&gt;
&lt;h2 id="moving-to-a-containerized-architecture"&gt;Moving to a Containerized Architecture&lt;/h2&gt;
&lt;p&gt;Zephyr&amp;rsquo;s application is the second generation of their online store. Their first generation was a monolith that they manually deployed. It was sufficient in Zephyr&amp;rsquo;s early days, but as word about Zephyr spread and the volume of traffic to their online store increased, Zephyr found it increasingly difficult to deploy small, incremental changes to the store&amp;rsquo;s code. As a result, the velocity of their development efforts slowed. In preparation for the next phase of their growth&amp;mdash;driven by robust sales of arcane artifacts&amp;mdash;Zephyr&amp;rsquo;s team evaluated a number of different architectures that would enable them to increase development velocity and give them greater flexibility in scaling different aspects of their online store. In the end, they settled on a containerized architecture deployed to &lt;a href="https://www.pulumi.com/kubernetes/"&gt;Kubernetes&lt;/a&gt; because some of the existing team was already familiar with these technologies.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a diagram of their second-generation application architecture:&lt;/p&gt;
&lt;p&gt;&lt;img src="app-architecture-vpc-eks-cluster.jpg" alt="A diagram depicting an application consisting of containerized microservices"&gt;&lt;/p&gt;
&lt;p&gt;As you can see in the diagram, the application has been broken down into microservices. The UI service communicates with other backend services via HTTP APIs; the various backend services communicate with various databases like MySQL and Redis for data persistence and/or caching. The backend services are responsible for implementing the functionality of a typical e-commerce site like orders, checking out, and managing the inventory catalog. While the choice of Kubernetes does afford Zephyr some much-needed functionality&amp;mdash;like the ability to easily scale different services within the application&amp;mdash;it also brings with it an added level of complexity that did not exist with their previous monolithic architecture. The Kubernetes manifest to deploy their application tops out at just over 1,000 lines of YAML!&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;Astute readers may note that Zephyr&amp;rsquo;s application looks very similar to &lt;a href="https://github.com/aws-containers/retail-store-sample-app/"&gt;this application&lt;/a&gt;; in truth, this series uses a fork of the AWS Containers retail store example as the application in this fictional scenario. (Many thanks to the AWS Containers DA team for their hard work here!) This is an admittedly over-engineered application, but it does afford the opportunity to address a range of customer scenarios in this blog series.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="managing-infrastructure-and-application-deployments"&gt;Managing Infrastructure and Application Deployments&lt;/h2&gt;
&lt;p&gt;As part of the switch to their new architecture, Zephyr decided they wanted to use Pulumi to manage both the infrastructure and the application deployment. Why Pulumi? Zephyr’s team recognized that moving to microservices on Kubernetes was going to make it more difficult to create reproducible, consistent implementations of their application. Zephyr&amp;rsquo;s team also knew that adopting infrastructure as code would help them with fast and repeatable deployments with little additional complexity. Another bonus was being able to use programming languages they already knew.&lt;/p&gt;
&lt;p&gt;As they prepared to embark on using Pulumi, a couple of questions came up for the Zephyr team:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Where should they store their Pulumi code?&lt;/strong&gt; The ability for Zephyr&amp;rsquo;s developers to use a full programming language to manage the infrastructure and deployment of their online store is a huge plus, but should this code go in the same repository as their application code? Or a different repository?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do they address the need for multiple environments?&lt;/strong&gt; It&amp;rsquo;s clear to the Zephyr development team that they&amp;rsquo;ll need multiple instances of the online store (for things like testing, QA, or development). What&amp;rsquo;s the best way to handle this when using Pulumi?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s examine each of these issues in a bit more detail, and see how Zephyr chose to proceed.&lt;/p&gt;
&lt;h3 id="storing-pulumi-code"&gt;Storing Pulumi Code&lt;/h3&gt;
&lt;p&gt;When it comes to answering the question of where to store Pulumi code relative to the application(s) it supports, there are two basic options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;In the same repository as your application code:&lt;/strong&gt; This approach has the advantage of being simpler, but may not offer the necessary flexibility when dealing with multiple teams or varying levels of access control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In a separate repository from your application code:&lt;/strong&gt; Using a separate repository adds some complexity, but does provide greater flexibility to more easily address a variety of organizational requirements and varying levels of access control.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So which approach is best? That will depend on a number of different factors, many of which are outlined in the &lt;a href="https://www.pulumi.com/docs/using-pulumi/organizing-projects-stacks/"&gt;organizing projects &amp;amp; stacks documentation&lt;/a&gt;. Some other factors that users need to take into consideration include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who is responsible for maintaining the code? If the application code and the Pulumi code are managed by different teams, then using separate repositories may be the best approach.&lt;/li&gt;
&lt;li&gt;What sort of access controls are needed? If the organization needs different access controls for the application code and the Pulumi code, then separate repositories are generally needed.&lt;/li&gt;
&lt;li&gt;What is the relationship between the infrastructure resources managed by Pulumi and the applications? If the infrastructure is &amp;ldquo;shared,&amp;rdquo; meaning it is used by multiple applications, then keeping the Pulumi code in a separate repository may be preferred. If the infrastructure is dedicated to a specific application, then keeping the application code and the Pulumi code together may be preferred.&lt;/li&gt;
&lt;li&gt;What about CI/CD? If an organization will use a &lt;a href="https://www.pulumi.com/what-is/what-is-ci-cd/"&gt;CI/CD pipeline&lt;/a&gt;, that may affect how you organize your code. (This is something we&amp;rsquo;ll delve into more deeply later in this series.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s worth stressing that the answers to these questions may change over time as organizations grow and their applications evolve. Addressing that change and its effects on an organization&amp;rsquo;s Pulumi projects and stacks is something we&amp;rsquo;ll be discussing later in this series.&lt;/p&gt;
&lt;div class="note note-info"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-info-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;The discussion above is working from the assumption that Pulumi users will store their Pulumi code in a version control system, like Git. Strictly speaking, using a version control system isn&amp;rsquo;t required for Pulumi to function (there is one exception we&amp;rsquo;ll touch on later in this series when we discuss &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/deployments/"&gt;Pulumi Deployments&lt;/a&gt;), but we &lt;strong&gt;very strongly&lt;/strong&gt; recommend that all users use Pulumi in conjunction with version control.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the case of Zephyr, their team felt like a monorepo approach (storing Pulumi code and application code together) was most appropriate for their specific requirements. Zephyr is a relatively small organization with a single team of developers who are responsible for managing both the cloud infrastructure as well as the development and deployment of the online store application. Zephyr&amp;rsquo;s team also decided to use a single Pulumi project&amp;mdash;for now. (Be sure to follow this blog series to see how that evolves, and why!)&lt;/p&gt;
&lt;h3 id="addressing-the-need-for-multiple-environments"&gt;Addressing the Need for Multiple Environments&lt;/h3&gt;
&lt;p&gt;The second question Zephyr encountered is how to handle the need for multiple instances of their application. In addition to a production instance&amp;mdash;which is the instance behind Zephyr&amp;rsquo;s public-facing online store&amp;mdash;Zephyr also felt they needed an environment for the developers to use in testing changes to the online store.&lt;/p&gt;
&lt;p&gt;This use case&amp;mdash;needing to have multiple, separate instances of the infrastructure and applications created by a single Pulumi program&amp;mdash;is exactly what &lt;a href="https://www.pulumi.com/docs/concepts/stack/"&gt;Pulumi stacks&lt;/a&gt; were designed to address. Each stack is a separate instance of the resources created by a Pulumi program within a project. Further, each stack has its own independent state, and each stack has its own configuration values. Stacks can be short-lived (meaning the associated resources are also short-lived), or stacks can be long-lived. Aside from the cloud resources created by the Pulumi program, stacks are lightweight and simple to create or delete.&lt;/p&gt;
&lt;p&gt;Zephyr decided to initially start with two stacks: a production stack (named &amp;ldquo;prod&amp;rdquo;) and a development stack (named &amp;ldquo;dev&amp;rdquo;). As you&amp;rsquo;ll observe throughout this series, this is a decision that is easily adjusted over time as your organization&amp;rsquo;s requirements change.&lt;/p&gt;
&lt;div class="rounded-lg bg-violet-50 p-6 my-8"&gt;
&lt;p class="heading-4 m-0 mb-3 flex items-center gap-1.5"&gt;Organize your infrastructure with stacks&lt;/p&gt;
&lt;div class="body-base m-0 text-gray-950"&gt;Model separate development, staging, and production environments as Pulumi stacks in a single project, each with its own state and configuration.&lt;/div&gt;
&lt;a href="https://app.pulumi.com/signup" data-track="blog-body-cta" class="btn btn-primary mt-4"&gt;
Get started
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--regular size-4" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.70121449e0dde6f8c01ff68423fffaa0336ecc73c7bbc87506404126694ca58c.svg#p-arrow-right-regular"/&gt;&lt;/svg&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id="viewing-the-first-iteration-of-code"&gt;Viewing the First Iteration of Code&lt;/h2&gt;
&lt;p&gt;You can view the first iteration of Zephyr&amp;rsquo;s Pulumi and application code&amp;mdash;the iteration that corresponds to the decisions described in this blog post&amp;mdash;by navigating to &lt;a href="https://github.com/pulumi/zephyr-app/"&gt;the Zephyr App GitHub repository&lt;/a&gt;. From the branch/tag dropdown, switch from the &lt;code&gt;main&lt;/code&gt; branch to viewing the &lt;a href="https://github.com/pulumi/zephyr-app/tree/blog/original/"&gt;&lt;code&gt;blog/original&lt;/code&gt;&lt;/a&gt; branch.&lt;/p&gt;
&lt;p&gt;From that GitHub repository, you can also choose to deploy the Pulumi code yourself. Full instructions for deploying the code are found in the repository.&lt;/p&gt;
&lt;h2 id="summarizing-best-practices-organizing-code-and-stacks"&gt;Summarizing Best Practices: Organizing Code and Stacks&lt;/h2&gt;
&lt;p&gt;This article introduced you to Zephyr, the fictional company for this blog series, and reviewed the application architecture for Zephyr&amp;rsquo;s online store (a containerized application running on Kubernetes). Throughout the blog series, this application serves as the use case around which various recommended practices are discussed.&lt;/p&gt;
&lt;p&gt;In this post the following guidelines were covered:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use version control for your Pulumi code.&lt;/strong&gt; Although it isn&amp;rsquo;t strictly required in many cases, a number of the benefits of adopting infrastructure as code are lost without the use of version control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Place your Pulumi code into version control repositories in a way that aligns with organizational requirements.&lt;/strong&gt; There is no &amp;ldquo;right&amp;rdquo; or &amp;ldquo;wrong&amp;rdquo; approach; choose the approach that enables you to satisfy your organization&amp;rsquo;s requirements around access control, code/repo ownership, and other organizational requirements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leverage Pulumi stacks to create multiple instances of cloud resources with a single Pulumi program.&lt;/strong&gt; Pulumi stacks were created to address the need for organizations to create multiple instances of cloud resources, each with its own state and its own set of configuration data, from a single Pulumi program.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For now, Zephyr has decided to go with a monorepo approach&amp;mdash;a single repository that contains both their application code and the Pulumi code to manage the infrastructure resources. All of the resources are defined in a single Pulumi project, with multiple stacks that correspond to development and production environments. Over the course of this series, we&amp;rsquo;ll see how Zephyr&amp;rsquo;s use of Pulumi changes as Zephyr grows and their application evolves.&lt;/p&gt;
&lt;p&gt;The next Zephyr blog post will examine how the Zephyr team adjusts their use of stacks and Git branches to support developer productivity. Stay tuned!&lt;/p&gt;</description><author>Aaron Kao</author><author>Christian Nunciato</author><author>Scott Lowe</author><category>best-practices</category><category>cloud-engineering</category><category>aws</category><category>eks</category></item></channel></rss>