Platform engineering과 internal developer platform: 개념과 적용
1. 개요
서비스 수가 증가하면, 각 팀이 CI/CD, 인프라, 로깅, 보안 및 문서를 직접 관리해야 합니다. 기능을 작성하는 대신 반복되는 작업에 많은 시간을 낭비합니다. Platform engineering은 이 문제를 이러한 작업들을 **internal developer platform (IDP)**으로 통합해 개발자가 스스로 서비스를 이용할 수 있도록 함으로써 해결합니다.
이 글을 읽은 후, 개념과 그것이 관심을 받는 이유를 이해할 것입니다. 또한 Spring Boot starter, service catalog과 간단한 policy check를 사용해 작은 “golden path”를 구축할 수 있을 것입니다. 글의 끝에는 platform에 투자하지 않아야 하는 경우들이 있습니다.
2. Platform engineering이란?
Platform engineering은 전담 팀(platform team)이 내부 도구, 프로세스 및 서비스를 구축하고 운영하는 업무입니다. 제품 팀(stream-aligned team)은 이 도구를 사용해 기본 인프라 계층을 깊이 있게 이해하지 않고도 서비스를 구축, 배포 및 운영할 수 있습니다.
2.1. Internal developer platform
IDP는 platform team이 만드는 제품입니다. 보통 하나의 도구가 아닙니다. 이것은 많은 기존 것들을 통합하는 계층입니다: Git, CI, Kubernetes, cloud, secret manager, observability. 개발자는 포털, CLI, 템플릿 또는 저장소의 구성 파일을 통해 이 계층과 상호 작용합니다.
2.2. 세 가지 핵심 아이디어
- Golden path (paved road): 일반적인 작업을 수행하기 위한 권장되고 사전 지원되는 경로입니다. 예를 들어 “새로운 REST 서비스 만들기”. 이 경로를 따르면 빠르고 안전합니다. 다른 경로를 가면 여전히 가능하지만, 팀이 책임을 져야 합니다.
- Self-service: 개발자가 누군가에게 티켓을 만들고 기다릴 필요 없이 서비스, 데이터베이스 또는 테스트 환경을 직접 만들 수 있습니다.
- Platform as a product: 내부 개발자가 고객입니다. Platform team은 그들과 인터뷰하고, 사용 수준을 측정하며, 실제 제품 팀처럼 백로그를 우선순위 지정해야 합니다.
2.3. DevOps와의 차이점은?
DevOps는 문화입니다: 코드를 작성한 팀도 운영에 책임이 있다는 의미입니다(“you build it, you run it”). Platform engineering은 DevOps를 대체하지 않습니다. 큰 규모에서 DevOps를 실현하는 한 방법입니다. 모든 팀이 Kubernetes, Terraform 및 IAM 전문가가 되도록 강요하는 대신, platform team은 이러한 지식을 쉽게 사용할 수 있는 추상화로 패키징합니다.
3. 왜 이 주제가 현재 주목받고 있나?
특정 도구에 의존하지 않는 실제적인 여러 이유가 있습니다:
- 인지 부하 증가: 현대적인 서비스는 Dockerfile, Kubernetes 매니페스트, 파이프라인, 대시보드, 알림, 보안 정책 및 비밀 구성이 필요할 수 있습니다. 각각에는 많은 선택지가 있습니다.
- 팀마다 다른 방식: 공통 표준이 없으면 각 서비스가 다른 형식으로 로깅하고 메트릭의 이름을 다르게 지정합니다. 문제 발생 시, 당직자가 어느 팀이 해당 서비스를 소유하는지 알기 어렵습니다.
- 보안 및 규정 준수 요구: dependency 스캔, artifact 서명, 접근 제어. 이러한 작업들은 golden path에 내장되면 균등하게 적용하기 쉽습니다.
- 느린 온보딩: 신입직원은 “서비스를 어디서 만드나, 어떻게 배포하나, 로그는 어디서 보나”를 알아야 합니다. 좋은 platform은 한 곳에서 이 모든 질문에 답합니다.
4. IDP의 일반적인 구성 요소
| 구성 요소 | 역할 |
|---|---|
| Service catalog | 서비스 목록, 소유자, 문서, 의존성 |
| Software template | 몇 가지 매개변수로 표준에 따라 새 프로젝트 생성 |
| 공유 CI/CD | 빌드, 테스트, 스캔, 배포가 포함된 기성 파이프라인 |
| 인프라 셀프 서비스 | 선언형 구성으로 데이터베이스, 큐, 버킷 생성 |
| 기본 Observability | 서비스 시작 시 기성 로그, 메트릭, 트레이스 |
| Guardrail / policy | 필수 규칙의 자동 확인 |
처음부터 모두 필요하지는 않습니다. 다음 섹션은 세 가지 작은 부분을 보여줍니다: golden path를 위한 Spring Boot starter, catalog descriptor 및 policy check.
5. Spring Boot starter를 사용한 golden path
Java 서비스를 표준화하는 가장 저렴한 방법은 내부 starter를 배포하는 것입니다. 이 starter를 추가한 서비스는 자동으로 표준 메타데이터를 가지게 됩니다: 이름, 소유 팀, 중요도. 메타데이터는 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를 사용하지 않는 서비스도 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입니다: “고아” 서비스가 없습니다.- 선언되지 않은 경우
tier에 기본값standard를 할당합니다.
5.3. Auto-configuration
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를 정의할 수 있습니다. 이것이 바로 golden path에서 벗어나는 “탈출구”입니다.- 중첩된 클래스
ActuatorConfiguration은InfoContributor가 classpath에 있을 때만 로드됩니다. 따라서 actuator를 사용하지 않는 서비스는ClassNotFoundException을 만나지 않습니다.
Spring Boot 3가 이 auto-configuration을 찾도록 하려면, src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에서 등록합니다:
dev.nonla.platform.PlatformServiceAutoConfiguration
서비스 쪽, 예를 들어 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는 이 endpoint를 스캔해 누가 어느 서비스를 소유하는지 알 수 있습니다.
5.4. Starter 동작 테스트
Platform 팀은 starter를 철저히 테스트해야 합니다. 여기서의 실수는 이를 사용하는 모든 서비스에 영향을 줍니다. ApplicationContextRunner는 전체 웹 서버를 시작하지 않고 auto-configuration을 빠르게 테스트하는 데 도움이 됩니다:
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. 저장소의 Catalog descriptor
대부분의 IDP는 service 메타데이터를 코드와 같은 저장소에 저장합니다. 다음은 오픈 소스 소스 code 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에 이 파일을 저장하면 두 가지 이점이 있습니다. 메타데이터가 코드와 함께 검토되고, 잊혀진 위키 페이지처럼 오래되지 않습니다. Portal은 이 파일을 읽어 소유자, 의존성 및 서비스의 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("name은 kebab-case이어야 하고, 3-41자여야 합니다");
}
if (!spec.owner().startsWith("team-")) {
errors.add("owner은 팀이어야 하고, 개인이 아니어야 합니다");
}
if (!ALLOWED_TIERS.contains(spec.tier())) {
errors.add("알 수 없는 tier: " + spec.tier());
}
if ("critical".equals(spec.tier()) && spec.replicas() < 2) {
errors.add("critical 서비스는 최소 2개의 replica가 필요합니다");
}
return errors;
}
}
입력 new ServiceSpec("Order_Service", "alice", "critical", 1)을 사용하면 함수는 다음을 반환합니다:
[name은 kebab-case이어야 하고, 3-41자여야 합니다, owner은 팀이어야 하고, 개인이 아니어야 합니다, critical 서비스는 최소 2개의 replica가 필요합니다]
중요한 점은 오류 메시지가 무엇을 수정해야 하는지를 명확하게 설명해야 한다는 것입니다. 좋은 guardrail은 개발자를 가로막을 뿐만 아니라 오류를 수정하도록 안내합니다.
7. 실제 적용: 어디서부터 시작할까?
- 먼저 개발자와 인터뷰하세요. 가장 많은 시간을 소비하는 곳을 물어보세요: 새 서비스 생성, 환경 프로비저닝 대기, 또는 파이프라인 디버깅. 가장 흥미로운 도구가 아니라 가장 큰 문제를 우선순위 지정하세요.
- “가장 얇은 실행 가능한 platform”으로 시작하세요. 때로는 템플릿 저장소, 공유 파이프라인 및 문서 페이지만으로 충분합니다. Portal은 나중에 올 수 있습니다.
- 하나의 golden path를 선택하세요. 예를 들어 “Kubernetes에 배포되는 Spring Boot REST 서비스”. 이 경로를 잘 만든 후에 확장하세요.
- 측정하세요. platform을 사용하는 팀의 수, 저장소 생성부터 첫 배포까지의 시간, 그리고 개발자의 정기적인 피드백을 추적하세요. 아무도 자발적으로 사용하지 않으면, 그것은 제품이 아직 준비되지 않았다는 신호입니다.
- 탈출구를 유지하세요. Golden path는 가장 쉬운 경로여야 하지, 유일한 필수 경로는 아닙니다. 섹션 5.3의
@ConditionalOnMissingBean은 이 원칙의 작은 예입니다. - 명확한 버전 관리 및 중단 공지. Starter, 템플릿 및 공유 파이프라인은 모두 API입니다. 호환성을 깨는 변경은 공지되고 업그레이드 경로가 있어야 합니다.
8. 일반적인 실수와 언제 사용하지 말아야 하는가
일반적인 실수:
- 콘텐츠가 있기 전에 portal을 구축합니다. 아름다운 portal이지만 catalog가 비어있고 템플릿이 오래되면 빠르게 잊혀집니다.
- Ops 팀의 이름을 platform team으로 바꿉니다. 개발자가 여전히 티켓을 만들고 기다려야 하면, 그것은 아직 self-service가 아닙니다.
- 너무 두꺼운 추상화. Kubernetes를 완전히 숨기면 개발자는 문제가 발생했을 때 디버깅할 수 없습니다. Platform은 인지 부하를 줄이되, 하위 계층을 보는 능력을 제거하지 않아야 합니다.
- 규정으로 강제한 가치. 명령으로만 사람을 움직이면, 그것은 platform이 실제 문제를 해결하지 못했다는 의미입니다.
- 모든 것을 하는 Starter. 너무 많은 의존성을 끌어오고 많은 암묵적 구성이 있는 starter는 버전 충돌을 유발하고 업그레이드하기 어렵습니다.
참고: Platform engineering에는 상당한 고정 비용이 있습니다. platform 자체를 구축, 운영 및 지원할 사람이 필요합니다.
IDP에 투자하지 말아야 할 때:
- 회사가 작고 몇 개의 팀과 서비스만 있습니다. 좋은 README와 공유 파이프라인이 보통 충분합니다.
- 제품이 여전히 product-market fit을 찾고 있고 아키텍처가 자주 변합니다. 너무 일찍 표준화하면 곧 바뀔 선택지를 잠그게 됩니다.
- Cloud의 관리 서비스가 이미 요구사항을 충족합니다. 비슷한 계층을 다시 구축하는 것은 유지 관리만 추가합니다.
- platform의 product owner가 될 누군가가 없습니다. 개발자의 말을 듣고 우선순위를 정할 사람이 없으면, platform은 아무도 사용하고 싶지 않은 도구 모음이 됩니다.
9. 결론
Platform engineering은 새로운 도구가 아닙니다. 이것은 일의 구조화 방식입니다: 한 팀이 내부 개발자의 경험을 제품으로 보고, 인프라 지식을 golden path와 self-service로 패키징합니다. Spring Boot starter가 test와 함께 메타데이터를 표준화하고, catalog descriptor가 저장소에 있고, policy check가 CI에서 실행되는 세 가지 작은 퍼즐 조각을 보았습니다.
작게 시작하고, 실제로 측정하고, 개발자가 실제로 사용할 때만 확장하세요. 조직이 아직 작으면, 좋은 템플릿과 공유 파이프라인이 완전한 platform 없이도 대부분의 이점을 제공할 수 있습니다.