--- name: platform-engineering description: Build internal developer platforms (IDPs) with self-service infrastructure, golden paths, and developer portals using Backstage, Crossplane, and score. license: MIT metadata: author: devops-skills version: "1.0" --- # Platform Engineering Platform engineering is the discipline of building and maintaining internal developer platforms (IDPs) that enable self-service capabilities for software engineering teams. The goal is to reduce cognitive load, standardize infrastructure provisioning, and accelerate delivery while maintaining governance and security guardrails. --- ## 1. When to Use Adopt platform engineering practices when your organization experiences: - **Cognitive overload on dev teams** -- developers spend more time on infrastructure wiring than writing business logic. - **Inconsistent environments** -- every team provisions infrastructure differently, causing drift and outages. - **Slow onboarding** -- new engineers take weeks to get a working development environment. - **Repeated toil** -- the same Terraform/Helm/CI boilerplate is copy-pasted across dozens of repos. - **Compliance bottlenecks** -- security and ops reviews gate every deployment, slowing release cadence. - **Scale inflection points** -- you have 5+ teams and shared infrastructure concerns (networking, observability, secrets). Platform engineering is NOT about replacing ops with a portal. It is about encoding organizational standards into reusable, self-service abstractions that dev teams consume through golden paths. --- ## 2. Backstage Setup [Backstage](https://backstage.io) is the leading open-source developer portal framework, originally created at Spotify. ### Installation ```bash # Prerequisites: Node.js 18+, yarn 1.x npx @backstage/create-app@latest # Follow the prompts -- name your app, e.g., "internal-platform" cd internal-platform # Start the development server yarn dev ``` ### Production Docker Build ```dockerfile # Dockerfile for Backstage production image FROM node:18-bookworm-slim AS build WORKDIR /app COPY package.json yarn.lock ./ COPY packages/ packages/ COPY plugins/ plugins/ RUN yarn install --frozen-lockfile RUN yarn tsc RUN yarn build:backend FROM node:18-bookworm-slim WORKDIR /app COPY --from=build /app/packages/backend/dist/ ./ COPY --from=build /app/node_modules/ ./node_modules/ COPY app-config.yaml app-config.production.yaml ./ ENV NODE_ENV=production CMD ["node", "packages/backend", "--config", "app-config.production.yaml"] ``` ### Core app-config.yaml ```yaml # app-config.yaml app: title: Internal Developer Platform baseUrl: http://localhost:3000 organization: name: MyOrg backend: baseUrl: http://localhost:7007 listen: port: 7007 database: client: pg connection: host: ${POSTGRES_HOST} port: ${POSTGRES_PORT} user: ${POSTGRES_USER} password: ${POSTGRES_PASSWORD} integrations: github: - host: github.com token: ${GITHUB_TOKEN} catalog: import: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration rules: - allow: [Component, System, API, Resource, Location, Template] locations: - type: url target: https://github.com/myorg/software-catalog/blob/main/catalog-info.yaml - type: url target: https://github.com/myorg/backstage-templates/blob/main/all-templates.yaml ``` --- ## 3. Crossplane for Self-Service Infrastructure Crossplane extends Kubernetes to provision and manage cloud infrastructure through declarative YAML. ### Install Crossplane ```bash # Add the Crossplane Helm repo helm repo add crossplane-stable https://charts.crossplane.io/stable helm repo update # Install Crossplane into its own namespace helm install crossplane crossplane-stable/crossplane \ --namespace crossplane-system \ --create-namespace \ --set args='{"--enable-composition-revisions"}' # Install the AWS provider kubectl apply -f - < s if lookup(s, "database", false) } source = "../rds-instance" name = "${var.team_name}-${each.key}" engine = "postgres" environment = var.environment } output "namespace" { value = module.namespace.name } output "kubeconfig_command" { value = "kubectl config set-context ${var.team_name}-${var.environment} --namespace=${module.namespace.name}" } ``` ### Environment Request CRD (Kubernetes Operator Pattern) ```yaml # environment-request.yaml apiVersion: platform.myorg.io/v1alpha1 kind: EnvironmentRequest metadata: name: commerce-staging namespace: platform-system spec: team: commerce environment: staging ttl: 72h # auto-cleanup for non-prod services: - name: orders-service port: 3000 replicas: 2 database: true - name: inventory-service port: 3001 replicas: 2 database: true - name: frontend port: 8080 replicas: 1 database: false notifications: slack: "#team-commerce-platform" ``` ### Backstage Self-Service Action (Custom Plugin) ```typescript // plugins/platform-actions/src/actions/provision-environment.ts import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; export const provisionEnvironmentAction = (config: Config) => { return createTemplateAction<{ team: string; environment: string; services: Array<{ name: string; port: number; replicas: number }>; }>({ id: 'platform:provision-environment', description: 'Provisions a complete environment for a team', schema: { input: { type: 'object', required: ['team', 'environment'], properties: { team: { type: 'string', title: 'Team Name' }, environment: { type: 'string', title: 'Environment', enum: ['dev', 'staging', 'prod'], }, services: { type: 'array', title: 'Services', items: { type: 'object', properties: { name: { type: 'string' }, port: { type: 'number' }, replicas: { type: 'number' }, }, }, }, }, }, }, async handler(ctx) { const { team, environment, services } = ctx.input; const platformApiUrl = config.getString('platform.apiUrl'); ctx.logger.info(`Provisioning ${environment} for team ${team}`); const response = await fetch(`${platformApiUrl}/environments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ team, environment, services }), }); if (!response.ok) { throw new Error(`Provisioning failed: ${response.statusText}`); } const result = await response.json(); ctx.logger.info(`Environment ready: ${result.namespace}`); ctx.output('namespace', result.namespace); ctx.output('dashboardUrl', result.dashboardUrl); }, }); }; ``` --- ## 9. Platform Metrics ### DORA Metrics Collection (Prometheus) ```yaml # prometheus-rules-dora.yaml apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: dora-metrics namespace: monitoring spec: groups: - name: dora.deployment_frequency interval: 1h rules: - record: dora:deployment_frequency:rate1d expr: | sum by (team, service) ( increase(argocd_app_sync_total{phase="Succeeded"}[1d]) ) - record: dora:deployment_frequency:rate7d expr: | sum by (team, service) ( increase(argocd_app_sync_total{phase="Succeeded"}[7d]) ) / 7 - name: dora.lead_time interval: 1h rules: - record: dora:lead_time_seconds:avg expr: | avg by (team, service) ( github_workflow_duration_seconds{workflow="deploy", status="success"} ) - name: dora.change_failure_rate interval: 1h rules: - record: dora:change_failure_rate:ratio expr: | sum by (team, service) ( increase(argocd_app_sync_total{phase="Failed"}[7d]) ) / sum by (team, service) ( increase(argocd_app_sync_total[7d]) ) - name: dora.mttr interval: 1h rules: - record: dora:mttr_seconds:avg expr: | avg by (team, service) ( pagerduty_incident_resolve_duration_seconds ) ``` ### Grafana Dashboard (JSON Model Snippet) ```json { "dashboard": { "title": "Platform Engineering -- DORA & Adoption", "panels": [ { "title": "Deployment Frequency (daily avg, 7d)", "type": "stat", "targets": [ { "expr": "dora:deployment_frequency:rate7d", "legendFormat": "{{team}}/{{service}}" } ] }, { "title": "Lead Time for Changes", "type": "gauge", "targets": [ { "expr": "dora:lead_time_seconds:avg / 3600", "legendFormat": "{{team}} (hours)" } ] }, { "title": "Change Failure Rate", "type": "gauge", "targets": [ { "expr": "dora:change_failure_rate:ratio * 100", "legendFormat": "{{team}} %" } ], "fieldConfig": { "defaults": { "thresholds": { "steps": [ { "color": "green", "value": 0 }, { "color": "yellow", "value": 15 }, { "color": "red", "value": 30 } ] } } } }, { "title": "Platform Adoption -- Scaffolded Repos", "type": "timeseries", "targets": [ { "expr": "sum(backstage_scaffolder_task_count_total{status='completed'})", "legendFormat": "Total scaffolded" } ] } ] } } ``` ### Developer Experience Survey (Automated Collection) ```yaml # cronjob-devex-survey.yaml apiVersion: batch/v1 kind: CronJob metadata: name: devex-survey-reminder namespace: platform-system spec: schedule: "0 10 1 */3 *" # quarterly, 1st of month at 10am jobTemplate: spec: template: spec: containers: - name: survey-bot image: myorg/platform-bot:latest env: - name: SLACK_WEBHOOK valueFrom: secretKeyRef: name: platform-bot-secrets key: slack-webhook - name: SURVEY_URL value: "https://forms.internal/devex-q1" command: - /bin/sh - -c - | curl -X POST "$SLACK_WEBHOOK" \ -H 'Content-Type: application/json' \ -d "{ \"text\": \"Hey team! It's time for our quarterly Developer Experience survey. Your feedback directly shapes platform priorities. Please take 5 minutes: ${SURVEY_URL}\" }" restartPolicy: OnFailure ``` --- ## 10. Governance -- Policy Enforcement ### OPA/Gatekeeper Constraint Templates ```yaml # constraint-template-approved-base-images.yaml apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: k8sapprovedbaseimages spec: crd: spec: names: kind: K8sApprovedBaseImages validation: openAPIV3Schema: type: object properties: allowedRegistries: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sapprovedbaseimages violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not startswith_any(container.image, input.parameters.allowedRegistries) msg := sprintf( "Container '%s' uses image '%s' which is not from an approved registry. Allowed: %v", [container.name, container.image, input.parameters.allowedRegistries] ) } startswith_any(str, prefixes) { prefix := prefixes[_] startswith(str, prefix) } --- apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sApprovedBaseImages metadata: name: approved-registries spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] namespaceSelector: matchExpressions: - key: platform.myorg.io/environment operator: Exists parameters: allowedRegistries: - "myorg.azurecr.io/" - "gcr.io/myorg-" - "public.ecr.aws/myorg/" ``` ### Kyverno Policies ```yaml # kyverno-require-labels.yaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-platform-labels annotations: policies.kyverno.io/title: Require Platform Labels policies.kyverno.io/description: >- All workloads must include standard platform labels for cost attribution, ownership tracking, and incident routing. spec: validationFailureAction: Enforce background: true rules: - name: check-required-labels match: any: - resources: kinds: - Deployment - StatefulSet - DaemonSet validate: message: >- All workloads must have the labels: platform.myorg.io/team, platform.myorg.io/environment, platform.myorg.io/cost-center. Found labels: {{request.object.metadata.labels}} pattern: metadata: labels: platform.myorg.io/team: "?*" platform.myorg.io/environment: "?*" platform.myorg.io/cost-center: "?*" - name: inject-default-security-context match: any: - resources: kinds: - Pod mutate: patchStrategicMerge: spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - (name): "*" securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL ``` ### Platform-Level Network Policies ```yaml # network-policy-platform-defaults.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: platform-default-deny namespace: "{{namespace}}" spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: platform.myorg.io/system: ingress-gateway - namespaceSelector: matchLabels: platform.myorg.io/system: monitoring podSelector: matchLabels: app: prometheus egress: - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53 - to: - namespaceSelector: matchLabels: name: "{{namespace}}" - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 ports: - protocol: TCP port: 443 ``` --- ## Summary A well-built internal developer platform combines these layers: | Layer | Tools | Purpose | |---|---|---| | Portal | Backstage | Single pane of glass for developers | | Catalog | catalog-info.yaml, APIs | Discoverability and ownership | | Golden Paths | Software Templates, Cookiecutter | Fast, standardized project scaffolding | | Self-Service Infra | Crossplane, Terraform | Declarative cloud resource provisioning | | Workload Spec | Score | Platform-agnostic app definitions | | Governance | OPA, Kyverno, Network Policies | Automated policy enforcement | | Metrics | DORA, DevEx surveys | Measure platform value and adoption | The platform team ships the platform as a product. Developers are the customers. Measure success by adoption, not by mandate.