January 03, 2026·6 min read
In Part 1, we built the foundation: a shared development cluster with namespace isolation and a quick-start template that gets developers from zero to deployed in under 5 minutes. In Part 2, we added preview environments that automatically spin up for every pull request.
Now it's time for production.
The Wrong Approach (Twice)
My first attempt at a production template was basically a copy of the quick-start template that pointed to a different cluster. Create a new repository, scaffold a new application, deploy to production. That was obviously wrong.
My second attempt was a "promote to production" template. A developer who already had a service in dev would run a separate template that created production infrastructure for their existing repo. Better, but still wrong.
Here's what I realized: if you know a service will eventually need production infrastructure, why not create it from the start?
The Insight: Production is Just Configuration
The difference between development and production isn't the code or the repository. It's configuration:
| Aspect | Development | Production |
|---|---|---|
| Cluster | shared-dev-cluster | aks-app-spoke |
| Namespace | team-dev | team-prod |
| Image tag | latest |
v1.0.0 (semver) |
| Replicas | 2 | 3+ |
| HPA | disabled | enabled |
| URL | {service}.chrishouse.io |
{service}-prod.chrishouse.io |
| Routing | Hub → East-West Gateway | Hub → Spoke Gateway |
If all of this is just YAML configuration, why can't we generate it all at once?
One Template, Both Environments
Instead of two separate templates (quick-start + promote), I merged them into a single golden path template with an optional production toggle:
parameters:
- title: Service Details
properties:
service_name:
title: Service Name
type: string
team:
title: Team
type: string
ui:field: OwnerPicker
description:
title: Description
type: string
- title: Production Configuration
properties:
includeProduction:
title: Include Production Infrastructure
type: boolean
default: true
description: Create production ArgoCD app, namespace, and ingress routes
prodReplicas:
title: Production Replicas
type: integer
default: 3
minimum: 2
enableHPA:
title: Enable Horizontal Pod Autoscaling
type: boolean
default: trueThe magic is in that includeProduction checkbox. Default is true, but developers can uncheck it if they're just experimenting.
What Gets Created
When a developer runs the template with production enabled, a single PR contains everything:
infra/
├── quickstart-services/my-service/
│ ├── namespace-claim.yaml # Dev namespace
│ ├── namespace-claim-prod.yaml # Prod namespace
│ ├── argocd-application.yaml # Dev ArgoCD app (tracks latest)
│ ├── argocd-application-prod.yaml # Prod ArgoCD app (tracks semver)
│ └── README.md
└── kubernetes/
└── istio-hub/
├── virtualservice-my-service-dev.yaml # Dev routing
└── virtualservice-my-service-prod.yaml # Prod routingSix files. One PR. Both environments ready.
The template itself has separate folders (gitops-manifests-dev/ and gitops-manifests-prod/) as building blocks that get conditionally included, but the output is unified - everything lands in the same PR.
Compare this to the two-template approach:
- Run quick-start template → PR #1
- Wait for approval and merge
- Develop for a while
- Run promote template → PR #2
- Wait for approval and merge
Now it's just:
- Run golden path template → PR #1
- Done
Conditional Steps in Backstage
The template uses conditional steps to skip production resources when not needed:
steps:
# Always runs - creates dev infrastructure
- id: create-gitops-dev
name: Create Dev GitOps Configuration
action: fetch:template
input:
url: ./gitops-manifests-dev
targetPath: ./gitops/quickstart-services/${{ parameters.service_name }}
# Only runs if production is enabled
- id: create-gitops-prod
name: Create Prod GitOps Configuration
if: ${{ parameters.includeProduction }}
action: fetch:template
input:
url: ./gitops-manifests-prod
targetPath: ./gitops/quickstart-services/${{ parameters.service_name }}
# Only runs if production is enabled
- id: create-hub-vs
name: Create Hub Ingress Route
if: ${{ parameters.includeProduction }}
action: fetch:template
input:
url: ./hub-virtualservice
targetPath: ./gitops/kubernetes/istio-hubThe if clause makes steps conditional. When includeProduction is false, the production steps are skipped entirely.
The Production ArgoCD Application
The production app uses ArgoCD Image Updater for automatic version detection:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-service-prod
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: "app=ghcr.io/crh225/my-service"
argocd-image-updater.argoproj.io/app.update-strategy: "semver"
argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^v[0-9]+\\.[0-9]+\\.[0-9]+$"
spec:
source:
repoURL: https://github.com/crh225/my-service
targetRevision: main
path: helm
helm:
parameters:
- name: image.tag
value: "v1.0.0" # Initial version
- name: replicaCount
value: "3"
- name: autoscaling.enabled
value: "true"
destination:
name: aks-app-spoke
namespace: team-prodThis creates a trunk-based release flow:
- Development: Push to main → builds
latest→ auto-deploys - Production: Create GitHub Release → builds
v1.x.x→ Image Updater detects and deploys
Traffic Flow
Both dev and prod environments get external URLs through the Istio mesh. The key difference is which cluster receives the traffic.
Development Traffic
https://my-service.chrishouse.io
│
▼
┌─────────────────────────────────────────┐
│ Hub Cluster (aks-mgmt-hub) │
│ │
│ main-gateway │
│ │ │
│ ▼ │
│ VirtualService: my-service-dev-vs │
│ │ │
│ ▼ │
│ ServiceEntry: shared-dev.internal │
│ → shared-dev east-west gateway IP │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Shared Dev Cluster │
│ │
│ cross-network-gateway (east-west) │
│ │ │
│ ▼ │
│ VirtualService: my-service │
│ │ │
│ ▼ │
│ my-service.team-dev.svc.cluster.local │
└─────────────────────────────────────────┘Production Traffic
https://my-service-prod.chrishouse.io
│
▼
┌─────────────────────────────────────────┐
│ Hub Cluster (aks-mgmt-hub) │
│ │
│ main-gateway │
│ │ │
│ ▼ │
│ VirtualService: my-service-prod-vs │
│ │ │
│ ▼ │
│ ServiceEntry: spoke.internal │
│ → app-spoke gateway IP │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ App Spoke Cluster (aks-app-spoke) │
│ │
│ cross-network-gateway (east-west) │
│ │ │
│ ▼ │
│ VirtualService: my-service │
│ │ │
│ ▼ │
│ my-service.team-prod.svc.cluster.local │
└─────────────────────────────────────────┘Both services are immediately accessible at their URLs after ArgoCD syncs. No port-forwarding, no VPN - just HTTPS to a real domain.
Why This Matters
This change is more significant than it might appear:
1. Reduced Cognitive Load
Developers don't need to learn two templates. There's one golden path that handles everything. The mental model is simpler: "run the template, get a service."
2. Production-Ready from Day One
Even if a developer doesn't release to production immediately, the infrastructure is waiting. When they're ready, it's just:
- Create a GitHub Release with tag
v1.0.0 - CI builds the image
- ArgoCD deploys to production
No additional PRs, no waiting for platform team approval again.
3. Consistent Configuration
When dev and prod are created together, they're guaranteed to be consistent. Same Helm chart, same value patterns, same team ownership. No drift between what was created in dev months ago and what gets created for prod now.
4. Faster Time to Production
The two-template approach had a hidden cost: the second template was often run weeks or months after the first. By then, developers had forgotten the exact configuration, team names had changed, and the promotion became a mini-project.
With everything created upfront, the path to production is just a git tag away.
5. Optional Complexity
The checkbox makes it optional. Experimenting with something that will never go to production? Uncheck the box. Building a real service? Leave it checked (the default).
The Complete Golden Path
The journey from idea to production is now:
| Stage | Action | What Happens |
|---|---|---|
| Create | Run golden path template | Dev + Prod infrastructure created |
| Develop | Push to main | Auto-deploys to dev cluster |
| Test | Open PR | Preview environment spins up |
| Release | Create GitHub Release v1.0.0 | Production deploys automatically |
One template. One PR. Both environments. Production when you're ready.
Developer Experience
From the developer's perspective:
- Open Backstage and select "Golden Path - Node.js Microservice"
- Enter service details (name, team, description)
- Configure production (replicas, HPA) or uncheck to skip
- Click create
The PR includes everything. After merge:
- Dev deploys immediately on every push to main
- Production waits for a GitHub Release
When ready for production:
- Go to your repository
- Create a Release with tag
v1.0.0 - Wait 2-3 minutes
- Service is live at
{service}-prod.chrishouse.io
Series Progress
- Part 1: Foundation - Shared clusters, namespace isolation, golden path template
- Part 2: Preview Environments - Ephemeral PR environments with Istio multi-cluster routing
- Part 3: Production from Day One (this post) - Optional production infrastructure in the initial template
Key Takeaways
-
Merge, don't separate. If dev and prod use the same code, create their infrastructure together.
-
Make it optional. A checkbox is all you need. Default to "yes" for real services.
-
Reduce the number of workflows. Every additional template is cognitive overhead. One template that does everything is better than specialized templates.
-
Production-ready doesn't mean production-deployed. Create the infrastructure upfront; deploy when ready via git tags.
-
Time to production matters. The faster developers can go from idea to production, the more value they deliver. Remove every unnecessary step.
The golden path is now truly golden: one template creates everything a service needs, from development through production. The only thing left is writing the code.
Implementation Repository
Full implementation: github.com/crh225/ARMServicePortal
Key files:
- Golden path template: backstage/templates/nodejs-quickstart/template.yaml
- Dev manifests: backstage/templates/nodejs-quickstart/gitops-manifests-dev/
- Prod manifests: backstage/templates/nodejs-quickstart/gitops-manifests-prod/
- Hub VirtualService template: backstage/templates/nodejs-quickstart/hub-virtualservice/
- Spoke VirtualService template: backstage/templates/nodejs-quickstart/spoke-virtualservice/
Enjoyed this post? Give it a clap!
- 1Platform Engineering Golden Paths: Common Patterns
- 2Building Golden Paths with Backstage: Part 1 - Foundation
- 3Building Golden Paths with Backstage: Part 2 - Preview Environments
- 4Building Golden Paths with Backstage: Part 3 - Production from Day OneReading
Comments