🏗Platform Engineering & IDP
The Senior DevOps Engineer of 2026 is a Platform Engineer. Build Internal Developer Platforms that abstract cloud complexity so 200 developers can self-serve. Master Backstage catalog, Crossplane XRDs for database-as-a-service, golden-path scaffolding, and DORA metrics dashboards. Reduce Mean Time to Onboard from 2 weeks to 2 hours.
The Problem: Developer Toil at Scale
Before Platform Engineering at a 300-person engineering org:
- New developer onboarding: 2–3 weeks of ticket-driven infra setup
- Creating a new microservice: 5 Jira tickets to DevOps team (ECR repo, IAM role, K8s namespace, Vault path, DNS entry)
- DevOps team: 70% of time on toil, 30% on actual platform work
- Shadow IT: devs spinning up EC2s directly to avoid the process
After IDP with Backstage + Crossplane: New service scaffold = 1 PR merge → 15 minutes → everything provisioned.
IDP Architecture 16.1
Portal + Catalog + TechDocs] Templates[Software Templates
Golden Paths] ArgoCD[ArgoCD
GitOps Controller] end subgraph "Infrastructure Plane" Crossplane[Crossplane
XRDs for Cloud Resources] TFC[Terraform Cloud
Complex IaC Modules] Vault[HashiCorp Vault
Dynamic Secrets] end subgraph "Cloud Resources" RDS[RDS PostgreSQL] S3[S3 Buckets] ECR[ECR Registry] NS[K8s Namespace + RBAC] end Dev -->|creates service| CLI CLI -->|scaffold template| Templates Templates -->|opens PR| PR PR -->|merge triggers| ArgoCD ArgoCD -->|reconciles| Crossplane Crossplane --> RDS Crossplane --> S3 Crossplane --> NS Backstage -->|reads catalog| ArgoCD
Backstage: Software Catalog & Golden Paths 16.2
Backstage is Spotify's open-source IDP that became the industry standard. It provides: (1) a Software Catalog — every service, API, team, and resource in one searchable UI; (2) Software Templates — interactive scaffolding wizards for developers; (3) TechDocs — docs-as-code rendered from markdown in Git.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-api
description: Payment processing microservice
annotations:
# Link to GitHub repo, Datadog dashboard, PagerDuty service
github.com/project-slug: acme-corp/payments-api
pagerduty.com/integration-key: abc123def456
grafana/dashboard-url: https://grafana.acme.com/d/payments
backstage.io/techdocs-ref: dir:.
labels:
language: python
domain: payments
tier: critical
tags:
- python
- fastapi
- kafka
- postgresql
spec:
type: service
lifecycle: production
owner: group:payments-team
system: payment-platform
dependsOn:
- resource:postgres-payments-db
- component:notification-service
providesApis:
- payments-v2-openapi
---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: postgres-payments-db
description: PostgreSQL RDS for payments service
annotations:
aws.amazon.com/arn: arn:aws:rds:us-east-1:123456:db:payments-prod
spec:
type: database
owner: group:platform-team
system: payment-platformapiVersion: 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, monitoring, and DB
tags: [python, fastapi, recommended]
spec:
owner: group:platform-team
type: service
parameters:
- title: Service Details
required: [serviceName, description, owner]
properties:
serviceName:
type: string
pattern: '^[a-z][a-z0-9-]{2,30}$'
description: Lowercase, hyphens only (e.g. order-processor)
description:
type: string
owner:
type: string
ui:field: OwnerPicker
- title: Infrastructure
properties:
needsDatabase:
type: boolean
default: false
title: PostgreSQL Database?
dbSize:
type: string
enum: [db.t3.micro, db.t3.small, db.m5.large]
default: db.t3.micro
ui:widget: radio
ui:options:
enumNames: ['Micro (dev/staging)', 'Small (SME prod)', 'Large (enterprise prod)']
needsKafka:
type: boolean
default: false
title: Kafka Topics?
steps:
- id: fetch-template
name: Fetch Base Template
action: fetch:template
input:
url: ./skeleton
values:
serviceName: ${{ parameters.serviceName }}
owner: ${{ parameters.owner }}
- id: create-github-repo
name: Create GitHub Repository
action: github:repo:create
input:
repoUrl: github.com?owner=acme-corp&repo=${{ parameters.serviceName }}
- id: provision-infrastructure
name: Provision Cloud Infrastructure
action: http:backstage:request
input:
method: POST
path: /api/proxy/crossplane
body:
serviceName: ${{ parameters.serviceName }}
needsDatabase: ${{ parameters.needsDatabase }}
dbSize: ${{ parameters.dbSize }}
- id: register-catalog
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['create-github-repo'].output.repoContentsUrl }}
output:
links:
- title: GitHub Repository
url: ${{ steps['create-github-repo'].output.remoteUrl }}
- title: Backstage Catalog Entry
icon: catalog
entityRef: ${{ steps['register-catalog'].output.entityRef }}site_name: 'Payments API'
site_description: 'Technical documentation for the payments service'
plugins:
- techdocs-core
nav:
- Home: index.md
- Architecture:
- Overview: architecture/overview.md
- Data Flow: architecture/data-flow.md
- Dependencies: architecture/dependencies.md
- Runbooks:
- On-Call Guide: runbooks/on-call.md
- Database Recovery: runbooks/db-recovery.md
- Rollback Procedure: runbooks/rollback.md
- API Reference: api.md
- ADRs:
- ADR-001 Chose FastAPI: adrs/001-fastapi.md
- ADR-002 Chose PostgreSQL: adrs/002-postgresql.md# Create new Backstage app $ npx @backstage/create-app@latest --skip-install $ cd my-backstage-app && yarn install # Configure GitHub auth in app-config.yaml $ cat > app-config.local.yaml << 'EOF' auth: providers: github: development: clientId: ${GITHUB_CLIENT_ID} clientSecret: ${GITHUB_CLIENT_SECRET} integrations: github: - host: github.com token: ${GITHUB_TOKEN} catalog: locations: - type: github-org target: https://github.com/acme-corp EOF # Start dev server $ yarn dev app started at http://localhost:3000 backend started at http://localhost:7007
Crossplane: Database-as-a-Service 16.3
Crossplane extends Kubernetes with Composite Resource Definitions (XRDs) — platform teams define abstract APIs (e.g., "I want a PostgreSQL database") and Crossplane provisions the actual cloud resources. Developers never touch AWS console.
# Platform team defines the abstract API
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.acme.io
spec:
group: database.acme.io
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance # What developers use
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [parameters]
properties:
parameters:
type: object
required: [size, team]
properties:
size:
type: string
enum: [small, medium, large]
description: "small=t3.micro, medium=m5.large, large=m5.2xlarge"
team:
type: string
description: "Team name for cost tagging"
version:
type: string
default: "15.4"
multiAZ:
type: boolean
default: falseapiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgresql-aws
spec:
compositeTypeRef:
apiVersion: database.acme.io/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
region: us-east-1
engine: postgres
autoMinorVersionUpgrade: true
backupRetentionPeriod: 7
storageEncrypted: true
deletionProtection: true
vpcSecurityGroupIdSelector:
matchLabels:
usage: rds
patches:
# Map size enum to actual instance type
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: spec.parameters.size
strategy: string
string:
fmt: |
%[1]s
transforms:
- type: map
map:
small: db.t3.micro
medium: db.m5.large
large: db.m5.2xlarge
toFieldPath: spec.forProvider.instanceClass
- fromFieldPath: spec.parameters.version
toFieldPath: spec.forProvider.engineVersion
- fromFieldPath: spec.parameters.multiAZ
toFieldPath: spec.forProvider.multiAZ
- fromFieldPath: spec.parameters.team
toFieldPath: spec.forProvider.tags.Team
- name: rds-password-secret
base:
apiVersion: secretsmanager.aws.upbound.io/v1beta1
kind: Secret
spec:
forProvider:
region: us-east-1
recoveryWindowInDays: 7# Developer just asks for a "PostgreSQL database"
# No knowledge of RDS, VPCs, subnets, security groups needed
apiVersion: database.acme.io/v1alpha1
kind: PostgreSQLInstance
metadata:
name: payments-db
namespace: payments-team # Lives in their own namespace
spec:
parameters:
size: medium # Platform team decided what "medium" means
team: payments
version: "15.4"
multiAZ: true
writeConnectionSecretToRef:
name: payments-db-creds # K8s Secret with host/user/password
---
# Developer then uses the connection secret in their Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
spec:
template:
spec:
containers:
- name: payments-api
envFrom:
- secretRef:
name: payments-db-creds # Auto-populated by CrossplaneDORA Metrics Dashboard 16.4
Elite Performer Targets
| Metric | Elite | High |
|---|---|---|
| Deployment Frequency | >1/day | 1/week |
| Lead Time for Changes | <1 hour | <1 week |
| MTTR | <1 hour | <1 day |
| Change Failure Rate | <5% | <10% |
Collecting DORA via GitHub API
# Deployment frequency from GitHub deployments
gh api repos/acme-corp/payments-api/deployments \
--paginate \
--jq '[.[] | select(.environment=="production")] | length'
# Lead time: PR merge → deployment
gh api repos/acme-corp/payments-api/pulls \
--paginate \
--jq '.[] | {
number: .number,
merged: .merged_at,
created: .created_at
}'
Hands-On Labs 16.5
Platform Engineering Labs
- Bootstrap Backstage locally with yarn, add GitHub integration, register 3 catalog entries
- Create a Software Template that scaffolds a Python service repo with CI/CD workflow
- Install Crossplane on kind, install AWS provider, create an XRD for S3 buckets
- Write a Crossplane Composition that provisions a K8s Namespace + RBAC + ResourceQuota as a bundle
- Build a DORA metrics script pulling deployment frequency from GitHub API over 30 days
- Set up TechDocs for a sample service — write runbook, architecture doc, and ADR pages
Troubleshooting 16.6
Check kubectl describe compositepgresqlinstance payments-db for events. Common causes: AWS provider credentials not configured (kubectl describe providerconfig aws), Composition not matching the XR's compositeTypeRef, or IRSA role missing required IAM permissions.
Check GitHub token scopes — needs repo and read:org. Verify catalog-info.yaml exists at repo root. Check Backstage backend logs for processor errors. Ensure the file is valid YAML with correct apiVersion.
Scaffolder actions run in Backstage backend. Check backend logs. github:repo:create needs a GitHub app or PAT with repo creation permission. Use the Backstage dry-run mode (--dry-run) to test templates locally.