平台工程与内部开发者平台:概念与应用

1. 总览

当 service 数量增加时,每个团队都必须自己处理 CI/CD、基础设施、日志、安全和文档。他们把大量时间花在重复的工作上,而不是编写功能。平台工程通过将这些工作整合到一个内部开发者平台 (IDP) 中来解决这个问题,让开发者可以自助服务。

读完这篇文章后,你将理解这个概念以及为什么它受关注。你也将能够通过 Spring Boot starter、服务目录和一个简单的 policy check 构建一条小的 “golden path”。文章最后是不应该投入平台工程的情况。

2. 什么是平台工程?

平台工程是指一个专门的团队 (platform team) 构建和运维一套工具、流程和内部服务。产品团队 (stream-aligned team) 使用这套系统来构建、部署和运维 service,而不需要深入理解底层基础设施的每一层。

2.1. 内部开发者平台

IDP 是 platform team 创建的产品。它通常不是单一的工具。它是一个集成层,整合了许多现有的东西:Git、CI、Kubernetes、cloud、secret manager、observability。开发者通过 portal、CLI、template 或者仅仅是 repo 中的一个配置文件与这个集成层交互。

2.2. 三个核心思想

  • Golden path (paved road): 推荐的和预先支持的路径,用于执行常见任务,例如“创建新的 REST service”。遵循这条路径又快又安全。你也可以走其他路径,但团队要自己承担责任。
  • Self-service: 开发者可以自己创建 service、数据库或测试环境,而不需要提工单等待其他人。
  • Platform as a product: 内部开发者是客户。Platform team 必须采访他们、衡量使用程度并优先处理 backlog,就像真正的产品团队一样。

2.3. 与 DevOps 有什么区别?

DevOps 是一种文化:编写代码的团队也负责运维(“you build it, you run it”)。平台工程不是替代 DevOps。它是在大规模实现 DevOps 的一种方式。与其强制每个团队都成为 Kubernetes、Terraform 和 IAM 专家,platform team 将这些知识打包成易用的抽象。

3. 为什么这个话题现在受到关注?

有几个实际的原因,与任何特定工具无关:

  1. 认知负担增加: 一个现代 service 可能需要 Dockerfile、Kubernetes manifest、pipeline、dashboard、alert、安全策略和 secret 配置。每样都有很多选择。
  2. 各队各有各的风格: 如果没有统一标准,每个 service 记录日志的格式不同,命名 metric 的方式也不同。当发生事故时,值班人员很难找出那个 service 属于哪个团队。
  3. 安全和合规要求: 扫描依赖项、签署 artifact、控制访问。如果这些都内置在 golden path 中,就容易统一应用。
  4. 缓慢的入职流程: 新人需要知道“在哪里创建 service、如何部署、在哪里查看日志”。一个好的 platform 可以在一个地方回答这些问题。

4. IDP 的常见组件

组件 作用
Service catalog Service 列表、所有者、文档、依赖项
Software template 仅用几个参数按标准创建新项目
共享 CI/CD 预配置的 pipeline 用于构建、测试、扫描、部署
Self-service 基础设施 通过声明式配置创建数据库、队列、bucket
默认 observability Service 启动时即可获得日志、指标、trace
Guardrail / policy 自动检查强制规则

你不需要从一开始就拥有所有这些。以下部分将说明三个小部分:用于 golden path 的 Spring Boot starter、catalog descriptor 和 policy check。

5. 通过 Spring Boot starter 实现 Golden path

标准化 Java service 最经济的方法是发布一个内部 starter。添加这个 starter 的 service 将自动拥有标准化的元数据:名称、所有者团队、重要性级别。元数据通过 actuator 展示。

5.1. 依赖项

内部 starter 需要以下依赖项:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Actuator 被标记为 optional。不使用 actuator 的 service 仍然可以使用 starter。

5.2. Properties 和 descriptor

package dev.nonla.platform;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("platform.service")
public record PlatformServiceProperties(String name, String owner, String tier) {

    public PlatformServiceProperties {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("platform.service.name 是必需的");
        }
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("platform.service.owner 是必需的");
        }
        if (tier == null || tier.isBlank()) {
            tier = "standard";
        }
    }
}
package dev.nonla.platform;

public record ServiceDescriptor(String name, String owner, String tier) {
}

record 的 compact constructor 做了两件事:

  • 强制必须声明 name 和 owner。缺少其中之一,应用就无法启动。这正是一个 guardrail:没有“孤立的” service。
  • 如果不声明,为 tier 分配默认值 standard。

5.3. 自动配置

package dev.nonla.platform;

import java.util.Map;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
@EnableConfigurationProperties(PlatformServiceProperties.class)
public class PlatformServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    ServiceDescriptor serviceDescriptor(PlatformServiceProperties props) {
        return new ServiceDescriptor(props.name(), props.owner(), props.tier());
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(InfoContributor.class)
    static class ActuatorConfiguration {

        @Bean
        InfoContributor platformInfoContributor(ServiceDescriptor descriptor) {
            return builder -> builder.withDetail("platform", Map.of(
                    "name", descriptor.name(),
                    "owner", descriptor.owner(),
                    "tier", descriptor.tier()));
        }
    }
}

逐部分解释:

  • @ConditionalOnMissingBean 允许 service 定义自己的 ServiceDescriptor。当真正需要时,这是“逃离” golden path 的方式。
  • 当 InfoContributor 在 classpath 上时,嵌套类 ActuatorConfiguration 才会被加载。这样不使用 actuator 的 service 就不会遇到 ClassNotFoundException。

要让 Spring Boot 3 找到这个自动配置,你需要在文件 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册它:

dev.nonla.platform.PlatformServiceAutoConfiguration

在 service 端,例如 order-service,只需要添加 starter 和几行配置:

platform.service.name=order-service
platform.service.owner=team-checkout
platform.service.tier=critical
management.endpoints.web.exposure.include=health,info
management.info.env.enabled=false

调用 GET /actuator/info 将获得:

{"platform":{"name":"order-service","owner":"team-checkout","tier":"critical"}}

从这里,监控工具或 catalog 可以扫描这个端点来了解谁拥有哪个 service。

5.4. 测试 starter 的行为

platform 团队必须仔细测试 starter。这里的一个错误会影响所有使用它的 service。ApplicationContextRunner 帮助快速测试自动配置,而无需启动整个 web server:

package dev.nonla.platform;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class PlatformServiceAutoConfigurationTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(PlatformServiceAutoConfiguration.class));

    @Test
    void givenNameAndOwner_whenStarting_thenDescriptorUsesDefaultTier() {
        runner.withPropertyValues(
                        "platform.service.name=order-service",
                        "platform.service.owner=team-checkout")
                .run(ctx -> {
                    assertThat(ctx).hasSingleBean(ServiceDescriptor.class)
                            .hasSingleBean(InfoContributor.class);
                    assertThat(ctx.getBean(ServiceDescriptor.class).tier())
                            .isEqualTo("standard");
                });
    }

    @Test
    void givenMissingOwner_whenStarting_thenContextFails() {
        runner.withPropertyValues("platform.service.name=order-service")
                .run(ctx -> {
                    assertThat(ctx).hasFailed();
                    assertThat(ctx.getStartupFailure())
                            .rootCause()
                            .hasMessageContaining("platform.service.owner");
                });
    }
}

第一个测试确认默认值和 InfoContributor 的注册。第二个测试确认 guardrail:缺少 owner 时 context 无法启动,错误消息清楚地指出缺少的属性。

6. Service catalog 和 guardrail

6.1. Repo 中的 Catalog descriptor

大多数 IDP 将 service 的元数据存储在 repo 中,与代码相邻。以下是开源代码 portal 中常见的 descriptor 类型:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  description: 处理订单和支付
  tags:
    - java
    - spring-boot
spec:
  type: service
  lifecycle: production
  owner: team-checkout
  system: checkout
  dependsOn:
    - resource:orders-db
  providesApis:
    - order-api

在 Git 中保存这个文件有两个好处。元数据与代码一起审查,不会像被遗忘的 wiki 页面那样偏离现实。Portal 只需要读取这个文件就能显示 service 的所有者、依赖项和 API。

6.2. 简单的 Policy check

Guardrail 不一定需要使用复杂的 policy engine。初期阶段,在 CI 中运行一段代码足以阻止配置错误:

package dev.nonla.platform.policy;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class ServicePolicy {

    private static final Set<String> ALLOWED_TIERS = Set.of("critical", "standard", "experimental");

    public record ServiceSpec(String name, String owner, String tier, int replicas) {
    }

    public List<String> violations(ServiceSpec spec) {
        List<String> errors = new ArrayList<>();
        if (!spec.name().matches("[a-z][a-z0-9-]{2,40}")) {
            errors.add("名称必须是 kebab-case,3-41 个字符");
        }
        if (!spec.owner().startsWith("team-")) {
            errors.add("所有者必须是团队,不是个人");
        }
        if (!ALLOWED_TIERS.contains(spec.tier())) {
            errors.add("未知的级别:" + spec.tier());
        }
        if ("critical".equals(spec.tier()) && spec.replicas() < 2) {
            errors.add("关键 service 需要至少 2 个副本");
        }
        return errors;
    }
}

对于输入 new ServiceSpec("Order_Service", "alice", "critical", 1),函数返回:

[名称必须是 kebab-case,3-41 个字符, 所有者必须是团队,不是个人, 关键 service 需要至少 2 个副本]

重点是错误消息必须清楚说明需要修改什么。好的 guardrail 指导开发者修复错误,而不仅仅是阻止他们。

7. 实际应用:从哪里开始?

  1. 首先采访开发者。 询问他们花最多时间在哪里:创建新 service、等待环境分配还是调试 pipeline。优先考虑最紧迫的问题,而不是最有趣的工具。
  2. 从“最小可行平台”开始。 有时只需要一个模板 repo、一个共享 pipeline 和一个文档页面。Portal 可以后来再做。
  3. 选择单一的 golden path。 例如“REST service 用 Spring Boot 部署到 Kubernetes”。先做好这条路径,然后再扩展。
  4. 测量。 跟踪使用 platform 的团队数量、从创建 repo 到首次部署的时间,以及开发者的定期反馈。如果没人自愿使用,这说明产品还不够好。
  5. 保持逃生路线。 Golden path 应该是最简单的路径,而不是唯一的强制路径。如第 5.3 节所示,@ConditionalOnMissingBean 是这个原则的一个小例子。
  6. 清晰的版本控制和弃用。 Starter、template 和共享 pipeline 都是 API。破坏兼容性的更改需要通知并提供升级路径。

8. 常见错误和何时不应使用

常见的错误:

  • 在有内容之前构建 portal。 一个漂亮的 portal,但 catalog 为空且 template 过时,会很快被遗忘。
  • 把 ops 团队改名为 platform team。 如果开发者仍然需要提工单等待,那还不是自助服务。
  • 过度抽象。 完全隐藏 Kubernetes 会导致开发者在出现问题时无法调试。Platform 应该减少认知负担,但不应该消除查看底层的能力。
  • 通过规定而不是价值来强制。 如果必须下命令人才会使用,那说明 platform 还没有解决真正的问题。
  • “做所有事情”的 Starter。 一个拉入太多依赖项和隐式配置的 starter 会导致版本冲突和难以升级。

注意: 平台工程有相当大的固定成本。你需要人来构建、维护和支持 platform 本身。

什么时候不应该投资单独的 IDP:

  • 小公司,只有几个团队和几个 service。一个好的 README 加上共享 pipeline 通常就足够了。
  • 产品仍在寻求 product-market fit,架构经常变化。过早标准化会锁定即将改变的选择。
  • 云的托管服务已经满足需求。自己重新构建类似的层只会增加维护工作。
  • 没有人愿意担任 platform 的 product owner。缺少倾听开发者的人,platform 很容易变成一套没人想用的工具集合。

9. 结论

平台工程不是新工具。这是组织工作的一种方式:一个团队将内部开发者的体验视为产品,然后将基础设施知识打包成 golden path 和自助服务。你已经看到了三个小部分:标准化元数据的 Spring Boot starter 加上测试、位于 repo 中的 catalog descriptor 以及在 CI 中运行的 policy check。

从小处开始,真实测量,只有在开发者真正使用时才扩展。如果你的组织还很小,一个好的 template 和一个共享 pipeline 可以带来大部分好处,而不需要完整的 platform。

platform-engineeringdevopsspring-bootdeveloper-experiencetech-trends