プラットフォームエンジニアリングと内部開発者プラットフォーム:概念と応用

1. 概要

サービスの数が増えるにつれ、各チームは自力でCI/CD、インフラストラクチャ、ログ、セキュリティ、ドキュメント管理をしなければなりません。彼らは機能の実装ではなく、繰り返しのタスクに多くの時間を費やしています。プラットフォームエンジニアリングはこの問題を解決します。これらのタスクを**内部開発者プラットフォーム(IDP)**にまとめ、開発者がセルフサービスで利用できるようにします。

この記事を読み終わると、その概念と関心が高まっている理由を理解できます。Spring Bootスターター、サービスカタログ、シンプルなポリシーチェックを使用して、小さな「ゴールデンパス」を構築することもできます。記事の後半では、プラットフォムに投資すべきでない場合についても説明します。

2. プラットフォームエンジニアリングとは?

プラットフォームエンジニアリングは、専任チーム(プラットフォームチーム)がツール、プロセス、内部サービスのセットを構築・運用するものです。プロダクトチーム(ストリーム配置チーム)はこのセットを使用して、下層のインフラストラクチャの詳細を理解することなく、サービスを構築、デプロイ、運用できます。

2.1. 内部開発者プラットフォーム

IDPはプラットフォームチームが作成する成果物です。通常、単一のツールではありません。既存のものを統合するレイヤーです:Git、CI、Kubernetes、クラウド、シークレット管理、可観測性。開発者はポータル、CLI、テンプレート、またはリポジトリ内の設定ファイルを通じてこのレイヤーと対話します。

2.2. 3つのコア概念

  • ゴールデンパス(舗装道): 一般的なタスク(例:「新しいRESTサービスを作成する」)を行うための推奨され、サポートされているパスです。このパスに従うと、速く安全です。別のパスを選ぶこともできますが、チームは責任を負います。
  • セルフサービス: 開発者は誰かを待ったりチケットを開くことなく、サービス、データベース、またはテスト環境を自分で作成できます。
  • プラットフォームアズプロダクト: 内部開発者が顧客です。プラットフォームチームは彼らにインタビューし、使用レベルを測定し、バックログを優先順位付けする必要があります。真の製品チームのようなものです。

2.3. DevOpsとの違いは?

DevOpsは文化です:コードを書くチームも運用を担当します(「you build it, you run it」)。プラットフォームエンジニアリングはDevOpsに取って代わりません。それは大規模でDevOpsを実現する方法です。各チームをKubernetes、Terraform、IAMの専門家にするのではなく、プラットフォームチームはその知識を使いやすいabstractionにパッケージします。

3. このトピックが注目されている理由

特定のツールに依存しない実際の理由があります:

  1. 認知負荷の増加: モダンなサービスはDockerfile、Kubernetesマニフェスト、パイプライン、ダッシュボード、アラート、セキュリティポリシー、シークレット設定が必要です。それぞれに多くの選択肢があります。
  2. チームごとに異なる方式: 共通の基準がないと、各サービスが異なる形式でログを記録し、メトリクスに異なる名前を付けます。インシデント発生時、オンコール担当者はそのサービスがどのチームに属するかを調べるのに苦労します。
  3. セキュリティとコンプライアンス要件: 依存関係のスキャン、アーティファクトへの署名、アクセス制御。ゴールデンパスに組み込まれていれば、これらは一貫して適用しやすいです。
  4. 遅いオンボーディング: 新入者は「サービスをどこに作成するか、デプロイ方法、ログを見る場所」を知る必要があります。優れたプラットフォームはこれらの質問に一か所で答えます。

4. IDPの一般的なコンポーネント

コンポーネント 役割
サービスカタログ サービス、オーナー、ドキュメント、依存関係のリスト
ソフトウェアテンプレート 数個のパラメータで標準に従うプロジェクトを作成
共有CI/CD ビルド、テスト、スキャン、デプロイを持つパイプラインが組み込み
インフラセルフサービス 宣言的な設定でデータベース、キュー、バケットを作成
デフォルト可観測性 サービス起動時にログ、メトリクス、トレースが組み込み
ガードレール / ポリシー 必須ルールを自動チェック

最初から全てを揃える必要はありません。以下の節では3つの小さなピース(Spring Bootスターターによるゴールデンパス、カタログディスクリプタ、ポリシーチェック)を示します。

5. Spring Bootスターターによるゴールデンパス

Javaサービスを標準化する最も安い方法は、内部スターターをリリースすることです。このスターターを追加するサービスは、自動的に標準メタデータを取得します:名前、担当チーム、重要度。メタデータはアクチュエータを通じて公開されます。

5.1. 依存関係

内部スターターに必要な依存関係:

<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>

アクチュエータはoptionalにマークされます。アクチュエータを使用しないサービスはまだスターターを使用できます。

5.2. プロパティとディスクリプタ

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 is required");
        }
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("platform.service.owner is required");
        }
        if (tier == null || tier.isBlank()) {
            tier = "standard";
        }
    }
}
package dev.nonla.platform;

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

Recordのコンパクトコンストラクタは2つのことを行います:

  • nameとownerの宣言を必須にします。どちらか一方を欠くと、アプリケーションは起動できません。これはガードレール:「孤児」サービスがありません。
  • 宣言されない場合、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はサービスが独自のServiceDescriptorを定義できるようにします。これはゴールデンパスから「逃げ道」です。本当に必要な場合に使用します。
  • ネストされたクラスActuatorConfigurationは、InfoContributorがclasspathにある場合のみロードされます。こうすることで、アクチュエータを使用しないサービスはClassNotFoundExceptionに遭遇しません。

Spring Boot 3がこのオートコンフィグレーションを見つけるには、src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsに登録します:

dev.nonla.platform.PlatformServiceAutoConfiguration

サービス側、例えばorder-serviceは、スターターと数行の設定を追加するだけです:

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"}}

ここから、監視ツールまたはカタログはこのエンドポイントをスキャンして、誰がどのサービスを所有しているかを知ることができます。

5.4. スターターの動作をテストする

プラットフォームチームはスターターを徹底的にテストする必要があります。ここでのバグは、それを使用しているすべてのサービスに影響を与えます。ApplicationContextRunnerは、Webサーバー全体を起動することなく、オートコンフィグレーションを素早くテストするのに役立ちます:

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の登録を確認します。2番目のテストはガードレールを確認します:ownerが欠けるとコンテキストは起動しません。エラーメッセージは欠落した属性を明確に示します。

6. サービスカタログとガードレール

6.1. リポジトリ内のカタログディスクリプタ

ほとんどのIDPはサービスのメタデータをリポジトリ内、コードのすぐそばに保存します。以下は、オープンソースソースコードポータルで一般的に見られるタイプのディスクリプタです:

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に保存することは2つの利点があります。メタデータはコードと一緒にレビューされ、忘れられたWikiページのように現実から外れることはありません。ポータルはこのファイルを読んで、オーナー、依存関係、およびサービスのAPIを表示するだけです。

6.2. シンプルなポリシーチェック

ガードレールは複雑なポリシーエンジンである必要はありません。スタートでは、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("name must be kebab-case, 3-41 chars");
        }
        if (!spec.owner().startsWith("team-")) {
            errors.add("owner must be a team, not a person");
        }
        if (!ALLOWED_TIERS.contains(spec.tier())) {
            errors.add("unknown tier: " + spec.tier());
        }
        if ("critical".equals(spec.tier()) && spec.replicas() < 2) {
            errors.add("critical services need at least 2 replicas");
        }
        return errors;
    }
}

入力new ServiceSpec("Order_Service", "alice", "critical", 1)の場合、関数は以下を返します:

[name must be kebab-case, 3-41 chars, owner must be a team, not a person, critical services need at least 2 replicas]

重要な点は、エラーメッセージが何を修正する必要があるかを明確に示す必要があります。良いガードレールは開発者をエラーの修正に導き、単に彼らをブロックするだけではありません。

7. 実践的な応用:どこから始めるか?

  1. 開発者にインタビューする。 彼らが最も時間を費やす場所を聞きます:新しいサービスの作成、環境のプロビジョニング待ち、またはパイプラインのデバッグ。最も興味深いツールではなく、最も痛い問題を優先します。
  2. 「最薄実行可能プラットフォーム」から始める。 しばしば必要なのは、テンプレートリポジトリ、共有パイプライン、ドキュメントページだけです。ポータルは後で来ることができます。
  3. 単一のゴールデンパスを選ぶ。 例えば「Spring BootのRESTサービスがKubernetesにデプロイ」。このパスをうまくいかせてから、拡張します。
  4. 測定する。 プラットフォームを使用するチーム数、リポジトリ作成から最初のデプロイまでの時間、および定期的な開発者フィードバックを追跡します。誰も自発的に使用しない場合、それは製品がまだ準備ができていないというシグナルです。
  5. 逃げ道を保つ。 ゴールデンパスは最も簡単なパスであり、唯一の必須パスではありません。セクション5.3のように、@ConditionalOnMissingBeanはこの原則の小さな例です。
  6. バージョニングと廃止を明確に。 スターター、テンプレート、共有パイプラインはAPIです。互換性を破る変更は通知され、アップグレードロードマップが必要です。

8. よくある間違いと使用すべきでない時

よくある間違い:

  • コンテンツの前にポータルを構築する。 美しいポータルでも、カタログが空でテンプレートが古いと、すぐに忘れられます。
  • opsチームの名前をプラットフォームチームに変更する。 開発者がまだチケットを開いて待つ必要があるなら、それはセルフサービスではありません。
  • abstractionが厚すぎる。 Kubernetesを完全に非表示にすると、問題が発生したときに開発者はデバッグできません。プラットフォームは認知負荷を減らすべきですが、下のレイヤーを見る能力を消してはいけません。
  • ルールの強制。 実行する必要がある場合、人々はそれを使用しています。つまり、プラットフォームはまだ本当の問題を解決していません。
  • 「すべてをする」スターター。 スターターが多くの依存関係と暗黙的な設定を引き込む場合、バージョンの競合を引き起こし、アップグレードが難しくなります。

注意: プラットフォームエンジニアリングには大きな固定コストがあります。プラットフォーム自体を構築、運用、サポートする人が必要です。

IDPに投資すべきでない場合:

  • 小さな企業、少数のチーム、少数のサービス。良いREADMEと共有パイプラインで十分なことがあります。
  • 製品がまだ製品市場フィットを探していて、アーキテクチャが頻繁に変わる。早すぎる標準化は、すぐに変わるべき選択肢を固定します。
  • クラウドの管理サービスは既にニーズを十分に満たしている。同じレイヤーを再構築するだけで、メンテナンスが増えるだけです。
  • プラットフォームのプロダクトオーナーになる意思のある人がいない。開発者の話を聞く人がいなければ、プラットフォームは誰も使いたくないツール集になりやすいです。

9. 結論

プラットフォームエンジニアリングは新しいツールではありません。それは組織の方法です:チームが内部開発者の経験を製品として見て、インフラ知識をゴールデンパスとセルフサービスにパッケージします。あなたは3つの小さなピースを見ました:メタデータを標準化するSpring Bootスターター(テスト付き)、リポジトリ内のカタログディスクリプタ、およびCI内で実行されるポリシーチェック。

小さく始め、実際に測定し、開発者が実際に使用する時だけ拡張します。組織がまだ小さい場合、優れたテンプレートと共有パイプラインが、完全なプラットフォームなしでほとんどの利点をもたらすことができます。

platform-engineeringdevopsspring-bootdeveloper-experiencetech-trends