platform-engineering
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlatform 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.
平台工程是构建和维护内部开发者平台(IDPs)的学科,旨在为软件工程团队提供自助服务能力。其目标是降低认知负担,标准化基础设施配置,加速交付速度,同时维持治理与安全防护措施。
1. When to Use
1. 适用场景
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.
当你的组织遇到以下情况时,可采用平台工程实践:
- 开发团队认知过载——开发者在基础设施配置上花费的时间多于编写业务逻辑。
- 环境不一致——每个团队的基础设施配置方式不同,导致环境漂移和故障。
- 入职流程缓慢——新工程师需要数周才能搭建好可用的开发环境。
- 重复繁琐工作——相同的Terraform/Helm/CI模板被复制粘贴到数十个仓库中。
- 合规瓶颈——每次部署都需要经过安全和运维审核,拖慢发布节奏。
- 规模拐点——你拥有5个及以上团队,且存在共享基础设施需求(网络、可观测性、密钥管理)。
平台工程并非用门户取代运维,而是将组织标准编码为可复用的自助式抽象,供开发团队通过黄金路径使用。
2. Backstage Setup
2. Backstage 搭建
Backstage is the leading open-source developer portal framework, originally created at Spotify.
Backstage是领先的开源开发者门户框架,最初由Spotify创建。
Installation
安装
bash
undefinedbash
undefinedPrerequisites: Node.js 18+, yarn 1.x
前置要求:Node.js 18+,yarn 1.x
npx @backstage/create-app@latest
npx @backstage/create-app@latest
Follow the prompts -- name your app, e.g., "internal-platform"
按照提示操作——为你的应用命名,例如"internal-platform"
cd internal-platform
cd internal-platform
Start the development server
启动开发服务器
yarn dev
undefinedyarn dev
undefinedProduction Docker Build
生产环境Docker构建
dockerfile
undefineddockerfile
undefinedDockerfile for Backstage production image
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"]
undefinedFROM 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"]
undefinedCore app-config.yaml
核心配置文件app-config.yaml
yaml
undefinedyaml
undefinedapp-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
---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
3. 基于Crossplane的自助式基础设施
Crossplane extends Kubernetes to provision and manage cloud infrastructure through declarative YAML.
Crossplane扩展Kubernetes功能,通过声明式YAML配置和管理云基础设施。
Install Crossplane
安装Crossplane
bash
undefinedbash
undefinedAdd the Crossplane Helm repo
添加Crossplane Helm仓库
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
Install Crossplane into its own namespace
将Crossplane安装到独立命名空间
helm install crossplane crossplane-stable/crossplane
--namespace crossplane-system
--create-namespace
--set args='{"--enable-composition-revisions"}'
--namespace crossplane-system
--create-namespace
--set args='{"--enable-composition-revisions"}'
helm install crossplane crossplane-stable/crossplane
--namespace crossplane-system
--create-namespace
--set args='{"--enable-composition-revisions"}'
--namespace crossplane-system
--create-namespace
--set args='{"--enable-composition-revisions"}'
Install the AWS provider
安装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
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
配置AWS凭证
kubectl create secret generic aws-creds
-n crossplane-system
--from-file=creds=./aws-credentials.txt
-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
undefinedkubectl create secret generic aws-creds
-n crossplane-system
--from-file=creds=./aws-credentials.txt
-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
undefinedCompositeResourceDefinition (XRD)
复合资源定义(XRD)
This defines a new platform API that developers consume without knowing the underlying cloud resources.
yaml
undefined该定义创建了一个新的平台API,开发者无需了解底层云资源即可使用。
yaml
undefinedxrd-application-database.yaml
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
undefinedapiVersion: 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
undefinedComposition (AWS RDS)
组合配置(AWS RDS)
yaml
undefinedyaml
undefinedcomposition-aws-database.yaml
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"
undefinedapiVersion: 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"
undefinedDeveloper Claim (what devs actually write)
开发者声明(实际编写内容)
yaml
undefinedyaml
undefinedmy-app-database.yaml
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
---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
4. 黄金路径
Golden paths are opinionated, well-supported paths through your tech stack that teams can follow to ship quickly.
黄金路径是技术栈中经过验证、支持完善的标准化流程,团队可遵循这些流程快速交付产品。
Backstage Software Template
Backstage软件模板
yaml
undefinedyaml
undefinedtemplate-nodejs-service.yaml
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 }}
undefinedapiVersion: 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 }}
undefinedCookiecutter Template Structure
Cookiecutter模板结构
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.yamljson
// cookiecutter.json
{
"service_name": "my-service",
"description": "A new microservice",
"owner_team": "platform",
"port": "3000",
"database": ["none", "postgres", "mysql"],
"node_version": "18"
}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.yamljson
// 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
5. 服务目录
catalog-info.yaml for a Microservice
微服务的catalog-info.yaml
yaml
undefinedyaml
undefinedcatalog-info.yaml
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
undefinedapiVersion: 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
undefinedAPI Entity
API实体
yaml
undefinedyaml
undefinedorders-api.yaml
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
undefinedapiVersion: 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
undefinedTechDocs Configuration
TechDocs配置
yaml
undefinedyaml
undefinedmkdocs.yml (in the service repo root)
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`:
```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-publishersite_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
在`app-config.yaml`中启用TechDocs:
```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-publisher6. Developer Portal -- Backstage Plugins
6. 开发者门户——Backstage插件
Kubernetes Plugin
Kubernetes插件
bash
undefinedbash
undefinedInstall Kubernetes plugin
安装Kubernetes插件
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
Backend configuration in `app-config.yaml`:
```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}yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
在`app-config.yaml`中配置后端:
```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)
CI/CD插件(GitHub Actions)
yaml
undefinedyaml
undefinedapp-config.yaml addition
app-config.yaml新增配置
proxy:
endpoints:
/github-actions:
target: https://api.github.com
headers:
Authorization: Bearer ${GITHUB_TOKEN}
Accept: application/vnd.github+json
undefinedproxy:
endpoints:
/github-actions:
target: https://api.github.com
headers:
Authorization: Bearer ${GITHUB_TOKEN}
Accept: application/vnd.github+json
undefinedMonitoring Plugin (Grafana)
监控插件(Grafana)
yaml
undefinedyaml
undefinedapp-config.yaml addition
app-config.yaml新增配置
grafana:
domain: https://grafana.internal
unifiedAlerting: true
proxy:
endpoints:
/grafana/api:
target: https://grafana.internal
headers:
Authorization: Bearer ${GRAFANA_API_TOKEN}
---grafana:
domain: https://grafana.internal
unifiedAlerting: true
proxy:
endpoints:
/grafana/api:
target: https://grafana.internal
headers:
Authorization: Bearer ${GRAFANA_API_TOKEN}
---7. Score Specification
7. Score规范
Score provides a platform-agnostic workload specification so developers describe what they need, and the platform decides how to provision it.
Score提供平台无关的工作负载规范,开发者只需描述需求,平台负责决定如何配置资源。
score.yaml
score.yaml
yaml
undefinedyaml
undefinedscore.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
undefinedapiVersion: 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
undefinedGenerating Platform-Specific Manifests
生成平台特定清单
bash
undefinedbash
undefinedInstall score-compose for local development
安装score-compose用于本地开发
brew install score-spec/tap/score-compose
brew install score-spec/tap/score-compose
Generate docker-compose from score.yaml
从score.yaml生成docker-compose配置
score-compose init
score-compose generate score.yaml
score-compose init
score-compose generate score.yaml
Install score-k8s for Kubernetes targets
安装score-k8s用于Kubernetes目标环境
brew install score-spec/tap/score-k8s
brew install score-spec/tap/score-k8s
Generate Kubernetes manifests from score.yaml
从score.yaml生成Kubernetes清单
score-k8s init
score-k8s generate score.yaml
---score-k8s init
score-k8s generate score.yaml
---8. Self-Service Workflows
8. 自助式工作流
Terraform Module Exposed via Platform API
通过平台API暴露的Terraform模块
hcl
undefinedhcl
undefinedmodules/environment/main.tf
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}"
}
undefinedvariable "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}"
}
undefinedEnvironment Request CRD (Kubernetes Operator Pattern)
环境请求CRD(Kubernetes Operator模式)
yaml
undefinedyaml
undefinedenvironment-request.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"
undefinedapiVersion: platform.myorg.io/v1alpha1
kind: EnvironmentRequest
metadata:
name: commerce-staging
namespace: platform-system
spec:
team: commerce
environment: staging
ttl: 72h # 非生产环境自动清理
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"
undefinedBackstage Self-Service Action (Custom Plugin)
Backstage自助式操作(自定义插件)
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);
},
});
};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
9. 平台指标
DORA Metrics Collection (Prometheus)
DORA指标采集(Prometheus)
yaml
undefinedyaml
undefinedprometheus-rules-dora.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
)undefinedapiVersion: 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
)undefinedGrafana Dashboard (JSON Model Snippet)
Grafana仪表盘(JSON模型片段)
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" }
]
}
]
}
}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
undefinedyaml
undefinedcronjob-devex-survey.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
-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
---apiVersion: batch/v1
kind: CronJob
metadata:
name: devex-survey-reminder
namespace: platform-system
spec:
schedule: "0 10 1 */3 *" # 每季度1日上午10点
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
-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
10. 治理——策略强制执行
OPA/Gatekeeper Constraint Templates
OPA/Gatekeeper约束模板
yaml
undefinedyaml
undefinedconstraint-template-approved-base-images.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/"
undefinedapiVersion: 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/"
undefinedKyverno Policies
Kyverno策略
yaml
undefinedyaml
undefinedkyverno-require-labels.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
undefinedapiVersion: 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
undefinedPlatform-Level Network Policies
平台级网络策略
yaml
undefinedyaml
undefinednetwork-policy-platform-defaults.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
---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.
一个构建完善的内部开发者平台包含以下层级:
| 层级 | 工具 | 用途 |
|---|---|---|
| 门户 | Backstage | 为开发者提供统一操作入口 |
| 目录 | catalog-info.yaml、APIs | 服务可发现性与所有权管理 |
| 黄金路径 | 软件模板、Cookiecutter | 快速标准化项目脚手架 |
| 自助式基础设施 | Crossplane、Terraform | 声明式云资源配置 |
| 工作负载规范 | Score | 平台无关的应用定义 |
| 治理 | OPA、Kyverno、网络策略 | 自动化策略强制执行 |
| 指标 | DORA、开发者体验调查 | 衡量平台价值与采用率 |
平台团队将平台作为产品交付,开发者是客户。通过采用率而非强制手段衡量成功与否。