platform-engineering
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 is the leading open-source developer portal framework, originally created at Spotify.
Installation
# 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 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 /app/packages/backend/dist/ ./
COPY /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
# 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
# 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 - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v1.1.0
EOF
# Configure AWS credentials
kubectl create secret generic aws-creds \
-n crossplane-system \
--from-file=creds=./aws-credentials.txt
kubectl apply -f - <<EOF
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: creds
EOF
CompositeResourceDefinition (XRD)
This defines a new platform API that developers consume without knowing the underlying cloud resources.
# xrd-application-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xapplicationdatabases.platform.myorg.io
spec:
group: platform.myorg.io
names:
kind: XApplicationDatabase
plural: xapplicationdatabases
claimNames:
kind: ApplicationDatabase
plural: applicationdatabases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
engine:
type: string
enum: ["postgres", "mysql"]
default: "postgres"
engineVersion:
type: string
default: "15"
storageGB:
type: integer
minimum: 20
maximum: 500
default: 20
instanceSize:
type: string
enum: ["small", "medium", "large"]
default: "small"
environment:
type: string
enum: ["dev", "staging", "prod"]
required:
- environment
Composition (AWS RDS)
# composition-aws-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: applicationdatabase-aws
labels:
provider: aws
spec:
compositeTypeRef:
apiVersion: platform.myorg.io/v1alpha1
kind: XApplicationDatabase
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
region: us-east-1
allocatedStorage: 20
autoMinorVersionUpgrade: true
backupRetentionPeriod: 7
dbName: appdb
deletionProtection: false
publiclyAccessible: false
skipFinalSnapshot: true
storageEncrypted: true
storageType: gp3
vpcSecurityGroupIdSelector:
matchLabels:
platform.myorg.io/network: shared
dbSubnetGroupNameSelector:
matchLabels:
platform.myorg.io/network: shared
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.engine
toFieldPath: spec.forProvider.engine
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.engineVersion
toFieldPath: spec.forProvider.engineVersion
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: spec.parameters.instanceSize
strategy: map
map:
small: db.t3.micro
medium: db.t3.medium
large: db.r6g.large
toFieldPath: spec.forProvider.instanceClass
- name: db-secret
base:
apiVersion: secretstores.aws.upbound.io/v1beta1
kind: Secret
spec:
forProvider:
region: us-east-1
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.name
transforms:
- type: string
string:
fmt: "platform/%s/db-credentials"
Developer Claim (what devs actually write)
# my-app-database.yaml
apiVersion: platform.myorg.io/v1alpha1
kind: ApplicationDatabase
metadata:
name: orders-db
namespace: team-commerce
spec:
parameters:
engine: postgres
engineVersion: "15"
storageGB: 50
instanceSize: medium
environment: staging
4. Golden Paths
Golden paths are opinionated, well-supported paths through your tech stack that teams can follow to ship quickly.
Backstage Software Template
# template-nodejs-service.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: nodejs-service
title: Node.js Microservice
description: Create a production-ready Node.js service with CI/CD, monitoring, and Kubernetes manifests.
tags:
- recommended
- nodejs
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required:
- name
- owner
- system
properties:
name:
title: Service Name
type: string
pattern: "^[a-z0-9-]+$"
ui:autofocus: true
description:
title: Description
type: string
owner:
title: Owner Team
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
system:
title: System
type: string
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: System
- title: Infrastructure
properties:
database:
title: Database
type: string
enum: ["none", "postgres", "mysql"]
default: "none"
cacheLayer:
title: Cache
type: string
enum: ["none", "redis"]
default: "none"
environment:
title: Target Environment
type: string
enum: ["dev", "staging", "prod"]
default: "dev"
steps:
- id: fetch-template
name: Fetch Skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
system: ${{ parameters.system }}
description: ${{ parameters.description }}
database: ${{ parameters.database }}
cacheLayer: ${{ parameters.cacheLayer }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ["github.com"]
repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
repoVisibility: internal
defaultBranch: main
protectDefaultBranch: true
requireCodeOwnerReviews: true
- id: create-argocd-app
name: Register with ArgoCD
action: argocd:create-resources
input:
appName: ${{ parameters.name }}
argoInstance: main
namespace: ${{ parameters.name }}
repoUrl: https://github.com/myorg/${{ parameters.name }}
path: deploy/k8s
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open in Backstage
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}
Cookiecutter Template Structure
golden-path-nodejs/
cookiecutter.json
{{cookiecutter.service_name}}/
Dockerfile
package.json
tsconfig.json
src/
index.ts
health.ts
deploy/
k8s/
deployment.yaml
service.yaml
ingress.yaml
.github/
workflows/
ci.yaml
deploy.yaml
catalog-info.yaml
// cookiecutter.json
{
"service_name": "my-service",
"description": "A new microservice",
"owner_team": "platform",
"port": "3000",
"database": ["none", "postgres", "mysql"],
"node_version": "18"
}
5. Service Catalog
catalog-info.yaml for a Microservice
# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: orders-service
description: Handles order creation, fulfillment, and tracking.
annotations:
backstage.io/techdocs-ref: dir:.
github.com/project-slug: myorg/orders-service
backstage.io/kubernetes-id: orders-service
backstage.io/kubernetes-namespace: team-commerce
argocd/app-name: orders-service
pagerduty.com/integration-key: ${PAGERDUTY_KEY}
grafana/dashboard-selector: "app=orders-service"
tags:
- nodejs
- grpc
links:
- url: https://grafana.internal/d/orders-service
title: Grafana Dashboard
icon: dashboard
- url: https://runbooks.internal/orders-service
title: Runbook
icon: docs
spec:
type: service
lifecycle: production
owner: team-commerce
system: commerce-platform
providesApis:
- orders-api
consumesApis:
- inventory-api
- payments-api
dependsOn:
- resource:orders-db
- resource:orders-cache
API Entity
# orders-api.yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: orders-api
description: Order management API
annotations:
backstage.io/techdocs-ref: dir:.
spec:
type: openapi
lifecycle: production
owner: team-commerce
system: commerce-platform
definition:
$text: ./api/openapi.yaml
TechDocs Configuration
# mkdocs.yml (in the service repo root)
site_name: Orders Service
site_description: Technical documentation for the Orders Service
nav:
- Home: index.md
- Architecture: architecture.md
- API Reference: api-reference.md
- Runbook: runbook.md
- ADRs:
- adr/001-choose-grpc.md
- adr/002-event-sourcing.md
plugins:
- techdocs-core
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
Enable TechDocs in app-config.yaml:
techdocs:
builder: external
generator:
runIn: docker
publisher:
type: awsS3
awsS3:
bucketName: myorg-techdocs
region: us-east-1
credentials:
roleArn: arn:aws:iam::123456789012:role/techdocs-publisher
6. Developer Portal -- Backstage Plugins
Kubernetes Plugin
# Install Kubernetes plugin
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
Backend configuration in app-config.yaml:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: https://k8s-dev.internal:6443
name: dev-cluster
authProvider: serviceAccount
serviceAccountToken: ${K8S_DEV_TOKEN}
skipTLSVerify: false
caData: ${K8S_DEV_CA_DATA}
- url: https://k8s-prod.internal:6443
name: prod-cluster
authProvider: serviceAccount
serviceAccountToken: ${K8S_PROD_TOKEN}
skipTLSVerify: false
caData: ${K8S_PROD_CA_DATA}
CI/CD Plugin (GitHub Actions)
# app-config.yaml addition
proxy:
endpoints:
/github-actions:
target: https://api.github.com
headers:
Authorization: Bearer ${GITHUB_TOKEN}
Accept: application/vnd.github+json
Monitoring Plugin (Grafana)
# app-config.yaml addition
grafana:
domain: https://grafana.internal
unifiedAlerting: true
proxy:
endpoints:
/grafana/api:
target: https://grafana.internal
headers:
Authorization: Bearer ${GRAFANA_API_TOKEN}
7. Score Specification
Score provides a platform-agnostic workload specification so developers describe what they need, and the platform decides how to provision it.
score.yaml
# score.yaml
apiVersion: score.dev/v1b1
metadata:
name: orders-service
tags:
team: commerce
tier: critical
containers:
main:
image: .
variables:
PORT: "3000"
DB_HOST: ${resources.db.host}
DB_PORT: ${resources.db.port}
DB_NAME: ${resources.db.name}
DB_USER: ${resources.db.username}
DB_PASSWORD: ${resources.db.password}
CACHE_HOST: ${resources.cache.host}
CACHE_PORT: ${resources.cache.port}
ports:
http:
port: 3000
protocol: TCP
readinessProbe:
httpGet:
path: /healthz
port: 3000
livenessProbe:
httpGet:
path: /healthz
port: 3000
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
resources:
db:
type: postgres
properties:
host:
port:
name:
default: orders
username:
password:
cache:
type: redis
properties:
host:
port:
dns:
type: dns
properties:
host:
sqs:
type: aws-sqs
properties:
queue_url:
arn:
service:
ports:
http:
port: 80
targetPort: 3000
Generating Platform-Specific Manifests
# Install score-compose for local development
brew install score-spec/tap/score-compose
# Generate docker-compose from score.yaml
score-compose init
score-compose generate score.yaml
# Install score-k8s for Kubernetes targets
brew install score-spec/tap/score-k8s
# Generate Kubernetes manifests from score.yaml
score-k8s init
score-k8s generate score.yaml
8. Self-Service Workflows
Terraform Module Exposed via Platform API
# modules/environment/main.tf
variable "team_name" {
type = string
description = "Name of the requesting team"
}
variable "environment" {
type = string
description = "Environment tier"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "services" {
type = list(object({
name = string
port = number
replicas = number
}))
description = "List of services to deploy"
}
module "namespace" {
source = "../k8s-namespace"
name = "${var.team_name}-${var.environment}"
labels = {
"platform.myorg.io/team" = var.team_name
"platform.myorg.io/environment" = var.environment
}
}
module "network_policy" {
source = "../network-policy"
namespace = module.namespace.name
allow_ingress_from = [
"istio-system",
"monitoring"
]
}
module "resource_quota" {
source = "../resource-quota"
namespace = module.namespace.name
cpu_limit = var.environment == "prod" ? "16" : "4"
mem_limit = var.environment == "prod" ? "32Gi" : "8Gi"
}
module "database" {
for_each = { for s in var.services : s.name => 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)
# 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)
// 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)
# 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)
{
"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)
# 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
# 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
# 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
# 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.