Syncing Environments with Shared Templates Across Teams
GuideSeptember 29, 202610 min read

Syncing Environments with Shared Templates Across Teams

The Short Version

A working bunnyshell.yaml gets copied from one team's repository to another's, tweaked for the new team's services, and from that moment on the two templates are independent, even though they started as the same file. Six months later, one team fixed a database connection-pooling bug in their copy, another team never got the fix, and a third team's copy has its own entirely separate variant of the same problem nobody has noticed yet. A shared template registry, one canonical version with each team's stack referencing it rather than forking it, is what prevents this specific and extremely common form of drift.

How Template Drift Actually Happens

It rarely happens through a deliberate decision to fork. It happens through the path of least resistance: a new team needs a bunnyshell.yaml and the fastest way to get one that already works is copying an existing team's file and editing the service names. This is a completely reasonable thing to do under time pressure, and it is also the exact moment a second, independently-evolving copy of what was previously one canonical pattern comes into existence. Nothing forces the two copies to stay in sync afterward, because nothing links them together at all once the copy is made.

The failure mode this produces is not dramatic, it's cumulative. A security-relevant fix (an IP restriction default, a secrets-handling pattern) applied to one team's copy does not propagate to the others, since there is no relationship between the files for it to propagate through. Months later, an audit or an incident surfaces that three of five teams are running a since-fixed pattern, and nobody remembers which copies got the fix and which didn't, because tracking that was never anyone's explicit job.

The Shared-Template Pattern

The structural fix is treating a bunnyshell.yaml as something teams reference, not something they own outright and diverge from. A central repository holds the canonical template (or templates, for teams with genuinely different stack shapes), and each team's environments are created against that canonical version rather than a locally copied one:

Bash
1$ bns environments create \
2    --from-template <CANONICAL_TEMPLATE_ID> \
3    --name "team-billing-pr-482" \
4    --project <BILLING_PROJECT_ID> \
5    --k8s <CLUSTER_ID>

Using --from-template with a shared template ID, instead of --from-path against a team-local copy of the file, is the concrete mechanism that keeps every team's environments pointed at the same source of truth. A team whose stack genuinely differs enough to need its own components still benefits from this by extending a shared base rather than starting from a fully independent copy, the same way application code shares a common base image rather than every service rebuilding its own from scratch.

What Belongs in the Shared Base vs Team-Specific Overrides

Not everything should live in the shared template. The parts worth centralizing are the conventions that should be identical everywhere for good reasons: how secrets are referenced (SECRET[] syntax, never a hardcoded value), how IP restriction is applied by default, how hostnames are patterned (prefix-{{ env.base_domain }}), and how shared variable groups are organized. The parts that should stay team-specific are the actual components: a billing team's stack has a payment-processing worker component no other team needs, and forcing that into a shared template would make the "shared" template a poor fit for everyone else.

YAML
1# Shared base: conventions, not specific components
2environmentVariablesGroups:
3  common:
4    LOG_LEVEL: info
5    ENABLE_TRACING: 'true'
6
7security:
8  access:
9    allowedIps:
10      - '203.0.113.0/24'  # Company-wide VPN range, every team inherits this default

A team extending this base adds its own components on top while inheriting the shared variable group and IP restriction default, rather than re-declaring both from scratch and risking a subtly different, unreviewed variant of each.

Rolling Out a Template Update Safely

Updating the canonical template is the easy part. Propagating that update to every environment already running against the old version needs a deliberate process, since a template update does not retroactively affect environments that already exist, only ones created or redeployed after the update. The real rollout process looks like this:

  1. Update the canonical template and commit the change with a clear description of what changed and why.
  2. Deploy the updated template to one test environment first and verify the affected components are actually healthy against it, not just that the deploy pipeline exits cleanly.
  3. Communicate the update to every team whose environments reference this template, with a clear timeline for when they should redeploy against it.
  4. Redeploy each team's long-lived (primary/staging) environments against the updated template on the communicated schedule.
  5. New ephemeral environments (PR previews) automatically pick up the updated template on their next creation, with no separate action needed, since they are created fresh each time.
Bash
$ bns environments update --id <TEAM_STAGING_ENV_ID> --from-template <CANONICAL_TEMPLATE_ID>

This distinction, ephemeral environments inheriting updates automatically on next creation versus long-lived environments needing an explicit redeploy step, is worth stating clearly to every team up front, since it is the single most common point of confusion in a multi-team rollout: someone assumes their staging environment already has the fix because "the template was updated last week," when in fact it was never redeployed against the new version.

Versioning the Template Itself

Treat template changes with the same discipline as application code changes: a version identifier (a git tag or a template registry version number) attached to each meaningful change, so any environment can report exactly which version of the shared template it was deployed from, and a rollback to a previous version is a known, tested operation rather than an improvised one.

YAML
1# template.yaml
2name: standard-node-stack
3description: 'Standard Node.js + Postgres stack, v2.3'
4tags: [nodejs, postgres, shared-base]
5discoverable: true

Following the standard update checklist (specific image version tags, never :latest, on every database and runtime component; shared values defined once at the environment level and referenced via {{ env.vars.X }} rather than repeated per component; security.access.allowedIps present with a comment explaining its purpose) before every template version bump catches the class of regression that otherwise surfaces as "why did team X's environment suddenly behave differently after last week's update" a week later, once someone finally traces it back.

Governance Without Bureaucracy

The point of centralizing templates is not introducing an approval bottleneck every team has to route small changes through. A lightweight model that works well in practice: any team can propose a change to the shared template via a normal pull request against the central template repository, a platform-owning team (or a rotating on-call role, for smaller organizations without a dedicated platform team) reviews it specifically for cross-team impact, not for the team's own specific use case, and the update rolls out following the process above once merged. This keeps the barrier to contributing low while still giving someone explicit responsibility for noticing when a proposed change would affect teams beyond the one proposing it.

What This Looks Like at Scale

For two teams sharing a similar stack, a shared template is a nice-to-have that prevents an early instance of drift. For an organization with a dozen teams, several with genuinely different stacks but similar underlying conventions, a shared template registry becomes closer to a requirement, since the alternative, a dozen independently drifting copies of what started as the same conventions, produces a slow-motion inconsistency that eventually surfaces as a security gap, a cost surprise, or simply a new engineer being confused about why two teams' "same" stack behaves differently in ways neither team can explain.

Handling a Team's Genuine Emergency Fix

Centralizing templates should never mean a team facing an actual production-adjacent emergency has to wait for a cross-team review before applying a fix to their own environment. The reasonable escape hatch is allowing a team to apply a local override for an urgent fix, clearly marked as temporary, with an explicit obligation to upstream the same fix to the shared template within a short, defined window afterward, rather than letting the local override quietly become the new permanent divergence.

YAML
1# Local override, explicitly marked and time-boxed
2# TEMPORARY: connection pool bumped for incident 2026-09-29.
3# Upstream this to the shared template by 2026-10-06 (see PLATFORM-ailed-431).
4environmentVariablesGroups:
5  common:
6    DB_POOL_SIZE: '50'  # shared template default is 20

The comment convention here matters more than it looks: a future reader (possibly the same person, months later) needs to immediately understand this is a deliberate, temporary divergence with a tracked follow-up, not an unexplained inconsistency someone will eventually have to reverse-engineer the reason for.

Detecting Drift Automatically Instead of Relying on Memory

Even with good discipline, some drift is inevitable over time, and catching it early is far cheaper than discovering it during a security review months later. A simple scheduled diff between each team's active environment configuration and the current shared template surfaces drift automatically, rather than depending on someone noticing:

Bash
1$ for team_env in billing-staging orders-staging shipping-staging; do
2    diff <(bns environments show --id "$team_env" -o yaml | yq '.environmentVariablesGroups.common') \
3         <(cat shared-template/base.yaml | yq '.environmentVariablesGroups.common') \
4      && echo "$team_env: in sync" || echo "$team_env: DRIFTED"
5  done
6
7billing-staging: in sync
8orders-staging: DRIFTED
9shipping-staging: in sync

Running this weekly, alongside the other scheduled audits described elsewhere in this cluster, turns drift detection into a routine, low-effort check rather than something that only gets noticed during an incident, when it's the most expensive time to discover it.

Onboarding a New Team Onto the Shared Base

The moment a shared template pays for itself most visibly is exactly the moment drift used to start: a new team standing up their first environment. Instead of pointing them at an existing team's repository to copy from, the onboarding path becomes referencing the shared base directly and adding only what's genuinely specific to their own stack.

YAML
1kind: Environment
2name: new-team-stack
3type: ephemeral
4
5# Inherited from the shared base template, not redeclared
6environmentVariablesGroups:
7  common:
8    LOG_LEVEL: info
9    ENABLE_TRACING: 'true'
10
11security:
12  access:
13    allowedIps:
14      - '203.0.113.0/24'
15
16# Genuinely new: this team's own components
17components:
18  - kind: Application
19    name: notifications-service
20    gitRepo: 'https://github.com/org/notifications.git'
21    hosts:
22      - hostname: 'notifications-{{ env.base_domain }}'
23        servicePort: 4000

A short onboarding checklist, "reference the shared template, add only your own components, don't redeclare shared conventions locally," takes less time to hand a new team than writing a bunnyshell.yaml from scratch would, and it's the single highest-leverage moment to prevent the next instance of drift before it starts, since it's much easier to start a new team off correctly than to reconcile their independent copy back into alignment later.

For the underlying pattern of one canonical environment definition producing many consistent copies within a single team, see One Staging Blueprint, Infinite Ephemeral Copies. For the programmatic side of triggering updates across many environments at once, see Automate Everything: Using the Bunnyshell Public API.

When It's Time to Split the Shared Base Into More Than One

A single shared template works well as long as most teams' stacks are genuinely similar. Once an organization has grown enough that it has, say, a cluster of Node.js services and a separate cluster of Python services with very different conventions, forcing both under one shared base starts producing awkward, conditional logic nobody's happy maintaining. The right response at that point isn't abandoning the shared-template pattern, it's splitting into two (or more) shared bases, one per genuinely distinct stack family, each still centralized and versioned within its own family, rather than reverting to every team maintaining an independent copy. The signal that it's time to split is usually a shared template accumulating more conditional branches for different stacks than it has genuinely shared conventions, which is the point where "shared" has stopped describing what the template actually is.

Checklist

  • New teams reference a shared canonical template instead of copy-pasting an existing team's file
  • Shared conventions (secrets syntax, IP restriction defaults, hostname patterns) live in the shared base, not duplicated per team
  • Template updates are versioned, with a clear rollout process distinguishing ephemeral (auto-updated) from long-lived (needs explicit redeploy) environments
  • A defined reviewer (platform team or rotating role) checks proposed template changes for cross-team impact
  • Every team knows the difference between "the template was updated" and "my environment picked up the update"
✦✦

Stop five teams from drifting five copies of the same template.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.

Frequently Asked Questions

Why would five teams end up with five different versions of a similar template in the first place?

Because copy-paste is the easiest way to start. A team building its second or third stack copies a working bunnyshell.yaml from an existing one, tweaks it for their own services, and from that point forward the two templates evolve independently, since nothing links them back together.

Does a shared template mean every team's stack has to look identical?

No. A shared base template covers the common structure (how variables are organized, how IP restriction is applied, how secrets are referenced) while each team's actual components still differ. The goal is shared conventions and shared fixes, not identical stacks.

What happens to environments that were created before a template update?

They keep running on whatever version of the template they were created from until they are explicitly redeployed against the updated template. A template update does not retroactively change environments that already exist, which is a safety property worth understanding, not a bug.

How do you actually propagate a security fix to every team's environments quickly?

By versioning the template, updating the central copy, and then triggering a redeploy across every environment referencing it, rather than relying on each team to notice the update and apply it manually on their own schedule.

Is a shared template registry overkill for a small team?

For one or two teams sharing a single stack, probably. It starts paying for itself once a third or fourth team is about to copy-paste an existing template, which is the exact moment drift starts, and catching it before the copy happens is much cheaper than reconciling divergence after the fact.