| name | platform-engineering |
| description | Build internal developer platforms (IDPs) with golden paths, self-service infrastructure, Backstage portals, and platform-as-a-product principles. Covers Backstage catalog, scaffolding templates, Crossplane, Argo CD GitOps, and measuring platform adoption with DORA metrics. |
| version | 1.0.0 |
| tags | ["platform-engineering","backstage","idp","crossplane","argocd","gitops","developer-experience","devops","kubernetes"] |
Platform Engineering
Overview
Platform engineering creates internal developer platforms (IDPs) that reduce cognitive load by providing golden paths — opinionated, self-service routes for common development tasks. Instead of every team re-solving infrastructure, CI/CD, and observability, a platform team builds reusable abstractions so product engineers deploy confidently without needing deep ops knowledge. The platform is treated as a product with internal customers, SLOs, and adoption metrics.
When to Use
- Engineering org has 10+ teams repeating the same infrastructure setup across services
- Developers wait days for infrastructure provisioning or environment setup
- Onboarding new engineers takes weeks due to undocumented, inconsistent tooling
- Security and compliance requirements need enforcement without blocking teams
- You want to measure and improve developer experience with DORA metrics (deploy frequency, lead time, MTTR, change failure rate)
- Teams are building their own CI/CD pipelines with no consistency or shared maintenance
Step-by-Step Workflow
1. Backstage Developer Portal Setup
npx @backstage/create-app@latest
cd my-backstage-app
yarn install
yarn dev
app:
title: Acme Developer Portal
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
cors:
origin: http://localhost:3000
catalog:
locations:
- type: url
target: https://github.com/acme-corp/catalog/blob/main/all-components.yaml
- type: github-org
target: https://github.com/acme-corp
auth:
providers:
github:
development:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Handles order processing and fulfillment
annotations:
github.com/project-slug: acme-corp/order-service
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P12345
argocd/app-name: order-service-prod
tags:
- python
- fastapi
- backend
links:
- url: https://grafana.acme.com/d/order-service
title: Grafana Dashboard
icon: dashboard
spec:
type: service
lifecycle: production
owner: group:platform/order-team
system: order-management
providesApis:
- order-api
dependsOn:
-
2. Scaffolding Templates (Golden Paths)
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: python-fastapi-service
title: Python FastAPI Microservice
description: Creates a production-ready FastAPI service with CI/CD, observability, and Kubernetes manifests
tags:
- python
- fastapi
- recommended
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required: [name, description, owner]
properties:
name:
title: Service Name
type: string
pattern: ^[a-z][a-z0-9-]*$
[]
[, , , ]
[, , , ]
[]
3. Crossplane — Infrastructure as Code for Self-Service
apiVersion: database.acme.com/v1alpha1
kind: PostgresDatabase
metadata:
name: order-service-db
namespace: order-team
spec:
parameters:
storageGB: 20
version: "15"
tier: standard
writeConnectionSecretToRef:
name: order-db-connection
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgres-rds
spec:
compositeTypeRef:
apiVersion: database.acme.com/v1alpha1
kind: PostgresDatabase
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
region: us-east-1
instanceClass:
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system --create-namespace
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-rds
spec:
package: xpkg.upbound.io/upbound/provider-aws-rds:v1.1.0
EOF
4. Argo CD GitOps for Deployments
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/acme-corp/platform-gitops
targetRevision: HEAD
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order-service
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/acme-corp/order-service
targetRevision: HEAD
path: k8s/overlays/production
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
argocd admin initial-password -n argocd
argocd login argocd.acme.com
argocd cluster add prod-context --name production
argocd app list
argocd app sync order-service
argocd app diff order-service
5. Platform Metrics with DORA
import httpx
from datetime import datetime, timedelta
from dataclasses import dataclass
@dataclass
class DORAMetrics:
deployment_frequency: float
lead_time_hours: float
change_failure_rate: float
mttr_hours: float
class DORACalculator:
def __init__(self, github_token: str, pagerduty_token: str):
self.gh = httpx.Client(
base_url="https://api.github.com",
headers={"Authorization": f"Bearer {github_token}"}
)
self.pd = httpx.Client(
base_url="https://api.pagerduty.com",
headers={"Authorization": f"Token token={pagerduty_token}"}
)
def deployment_frequency(self, repo: str, days: int = 30) -> float:
"""Deployments per day from GitHub releases/tags."""
since = (datetime.now() - timedelta(days=days)).isoformat() + "Z"
resp = self.gh.get(,
params={: })
releases = [r r resp.json()
r[] > since]
(releases) / days
() -> :
resp = .gh.get(,
params={: })
lead_times = []
deploy resp.json():
env = deploy.get(, )
env:
sha = deploy[]
commit = .gh.get().json()
commit_time = datetime.fromisoformat(
commit[][][].rstrip())
deploy_time = datetime.fromisoformat(
deploy[].rstrip())
lead_times.append((deploy_time - commit_time).total_seconds() / )
(lead_times) / (lead_times) lead_times
() -> :
since = (datetime.now() - timedelta(days=days)).isoformat()
incidents = .pd.get(, params={
: service,
: since,
:
}).json()[]
deploy_count = (.deployment_frequency(service, days) * days)
(incidents) / deploy_count deploy_count
() -> :
results = {}
svc services:
freq = .deployment_frequency(svc)
lead = .lead_time(svc)
cfr = .change_failure_rate(svc)
results[svc] = {
: freq,
: freq >= freq >= / ,
: lead,
: lead < lead < ,
: cfr,
: cfr < cfr < ,
}
results
Key Commands Reference
npx @backstage/create-app@latest
yarn backstage-cli package start
yarn tsc
yarn build:all
kubectl get managed
kubectl get composite
kubectl describe postgresqldatabase order-db
kubectl get events --field-selector reason=CannotObserveExternalResource
argocd app list
argocd app get order-service
argocd app sync order-service
argocd app rollback order-service 3
argocd proj list
kubectl port-forward svc/argocd-server -n argocd 8080:443
npx @techdocs/cli generate --source-dir . --output-dir ./site
npx @techdocs/cli serve
Common Patterns
Pattern 1: Environment-Per-Team with Namespace Isolation
apiVersion: v1
kind: Namespace
metadata:
name: order-team
labels:
team: order-team
cost-center: "CC-1234"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-team-developers
namespace: order-team
subjects:
- kind: Group
name: acme-corp:order-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: order-team-quota
namespace: order-team
spec:
hard:
requests.cpu: "8"
Pattern 2: Internal Platform API with Self-Service Provisioning
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
import subprocess, uuid
app = FastAPI(title="Platform API")
class ServiceRequest(BaseModel):
name: str
team: str
language: str
database: str | None = None
class ProvisionStatus(BaseModel):
job_id: str
status: str
repo_url: str | None = None
argocd_url: str | None = None
jobs: dict[str, ProvisionStatus] = {}
@app.post("/services", response_model=ProvisionStatus)
async def create_service(req: ServiceRequest, bg: BackgroundTasks):
job_id = str(uuid.uuid4())
jobs[job_id] = ProvisionStatus(job_id=job_id, status="pending")
bg.add_task(provision_service, job_id, req)
return jobs[job_id]
async def provision_service(job_id: , req: ServiceRequest):
jobs[job_id].status =
:
result = subprocess.run([
, ,
, ,
,
], capture_output=, text=, check=)
jobs[job_id].status =
jobs[job_id].repo_url =
subprocess.CalledProcessError e:
jobs[job_id].status =
():
jobs[job_id]
Pattern 3: Platform Health Dashboard Query
import httpx, json
def platform_adoption_report(backstage_url: str, token: str) -> dict:
"""What % of services use golden path templates vs custom setup."""
client = httpx.Client(
base_url=backstage_url,
headers={"Authorization": f"Bearer {token}"}
)
components = client.get("/api/catalog/entities",
params={"filter": "kind=component,spec.type=service"}).json()
total = len(components["items"])
golden_path = sum(
1 for c in components["items"]
if c.get("metadata", {}).get("annotations", {}).get("scaffolded-by-platform") == "true"
)
with_techdocs = sum(
1 for c in components["items"]
if "backstage.io/techdocs-ref" in c.get("metadata", {}).get("annotations", {})
)
return {
"total_services": total,
"golden_path_adoption": f"{100*golden_path/total:.1f}%",
"techdocs_adoption": f"%",
}
Pitfalls to Avoid
-
Building a platform no one asked for: The most common failure is a platform team building abstractions based on their own opinions without continuous feedback from developer customers. Run quarterly developer experience surveys (SPACE framework), treat pain points as a backlog, and measure adoption — not just feature delivery. A golden path no one uses is just extra maintenance.
-
Making the golden path mandatory before it's golden: If you force teams onto your platform before it handles their edge cases, they'll route around it and you'll lose trust. Start with a small pilot team, iterate until the path is genuinely better than DIY, then expand. The "pave the path people are already walking" approach beats imposing structure from above.
-
No versioning or migration strategy for platform changes: When you change a template, Crossplane composition, or Argo CD policy, every service built on it is affected. Treat breaking changes like library releases — version your APIs, provide migration guides, and give teams a deprecation window before removing old platform features.
Related Skills
kubernetes-architect — Cluster design, multi-tenancy, node pool strategy
ci-cd-pipeline-builder — GitHub Actions and pipeline patterns the platform wraps
senior-devops — Broader SRE and operations practices
service-mesh-istio — Service mesh layer that platform teams often manage
api-gateway-design — Gateway patterns for internal platform APIs
GitNexus Index
{
"skill": "platform-engineering",
"category": "devops",
"triggers": ["platform engineering", "internal developer platform", "IDP", "backstage", "golden path", "developer portal", "crossplane", "argocd", "gitops", "DORA metrics", "self-service infrastructure"],
"outputs": ["app-config.yaml", "catalog-info.yaml", "scaffolding template", "Crossplane composition", "Argo CD Application", "DORACalculator"],
"complexity": "high",
"tools"