Platform as a Product 관점에서의 API 계층 설계, 추상화 수준 결정, 내부 API 버전닝, 인증과 인가, 감사 로깅, 그리고 CLI 도구 제공까지 다룹니다.
플랫폼을 성공적으로 운영하려면 내부 제품으로 취급해야 합니다. 개발자는 고객이고, 플랫폼 팀은 제품 팀입니다.
| 요소 | 외부 제품 | 플랫폼 (내부 제품) |
|---|---|---|
| 고객 | 외부 사용자 | 내부 개발자 |
| 제품 관리자 | Product Manager | Platform Product Manager |
| 성공 지표 | MAU, 매출 | 도입률, 개발자 만족도 |
| 피드백 | 사용자 리뷰 | 개발자 설문, NPS |
| 문서 | API 문서 | 플랫폼 가이드, Runbook |
| 지원 | 고객 지원팀 | Slack 채널, Office Hour |
Backstage 포털은 개발자 경험의 "프론트엔드"입니다. 하지만 플랫폼의 진정한 가치는 API 계층에 있습니다. 잘 설계된 플랫폼 API는 다음을 가능하게 합니다.
플랫폼 API는 세 가지 추상화 수준으로 설계합니다.
Level 3: 워크플로우 API (최고 수준 추상화)
- "새 마이크로서비스 생성" (한 번의 호출로 모든 것 완료)
- "서비스를 production에 배포" (복잡한 과정을 한 번에)
Level 2: 리소스 API (중간 수준 추상화)
- "PostgreSQL 데이터베이스 생성"
- "Redis 캐시 생성"
- "네임스페이스 생성"
Level 1: 기반 API (최저 수준 추상화)
- "Terraform 워크스페이스 실행"
- "Kubernetes 매니페스트 적용"
- "ArgoCD 앱 동기화"대부분의 개발자는 Level 3(워크플로우 API)를 사용합니다. 고급 사용자나 자동화 파이프라인은 Level 2(리소스 API)를 사용합니다. Level 1은 플랫폼 팀 내부에서만 사용합니다.
openapi: 3.1.0
info:
title: "Platform API"
version: "1.0.0"
description: "내부 개발자 플랫폼 API"
paths:
/api/v1/services:
post:
summary: "새 서비스 생성"
description: "Golden Path 기반으로 새 마이크로서비스를 생성합니다."
tags: [services]
requestBody:
content:
application/json:
schema:
type: object
required: [name, template, owner]
properties:
name:
type: string
pattern: "^[a-z][a-z0-9-]*$"
example: "checkout-service"
template:
type: string
enum: ["spring-boot", "go-grpc", "fastapi", "nextjs"]
owner:
type: string
example: "team-checkout"
system:
type: string
example: "checkout-system"
options:
type: object
properties:
database:
type: string
enum: ["postgresql", "mysql", "none"]
messaging:
type: string
enum: ["kafka", "rabbitmq", "none"]
responses:
"202":
description: "서비스 생성이 시작되었습니다."
content:
application/json:
schema:
type: object
properties:
taskId:
type: string
example: "task-abc123"
status:
type: string
example: "provisioning"
estimatedTime:
type: string
example: "5-10 minutes"
/api/v1/services/{name}:
get:
summary: "서비스 상세 조회"
tags: [services]
parameters:
- name: name
in: path
required: true
schema:
type: string
responses:
"200":
description: "서비스 상세 정보"
/api/v1/infrastructure/databases:
post:
summary: "데이터베이스 프로비저닝"
tags: [infrastructure]
requestBody:
content:
application/json:
schema:
type: object
required: [name, engine, size]
properties:
name:
type: string
engine:
type: string
enum: ["postgresql", "mysql"]
size:
type: string
enum: ["small", "medium", "large"]
environment:
type: string
enum: ["dev", "staging", "production"]
responses:
"202":
description: "데이터베이스 프로비저닝이 시작되었습니다."
/api/v1/costs/{service}:
get:
summary: "서비스별 비용 조회"
tags: [costs]
parameters:
- name: service
in: path
required: true
schema:
type: string
- name: period
in: query
schema:
type: string
enum: ["7d", "30d", "90d"]
default: "30d"
responses:
"200":
description: "비용 데이터"플랫폼 API는 비동기 패턴을 적극 활용해야 합니다. 인프라 프로비저닝은 수 분에서 수십 분이 걸릴 수 있으므로, 즉각 응답(202 Accepted)과 함께 작업 상태를 조회할 수 있는 엔드포인트를 제공합니다.
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import { ScaffolderClient } from '../clients/scaffolder';
import { AuditLogger } from '../audit/logger';
import { CostEstimator } from '../costs/estimator';
const CreateServiceSchema = z.object({
name: z.string().regex(/^[a-z][a-z0-9-]*$/),
template: z.enum(['spring-boot', 'go-grpc', 'fastapi', 'nextjs']),
owner: z.string(),
system: z.string().optional(),
options: z.object({
database: z.enum(['postgresql', 'mysql', 'none']).default('none'),
messaging: z.enum(['kafka', 'rabbitmq', 'none']).default('none'),
}).optional(),
});
const router = Router();
router.post('/api/v1/services', async (req: Request, res: Response) => {
// 입력 검증
const parsed = CreateServiceSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.issues,
});
}
const input = parsed.data;
const userId = req.user?.id;
// 비용 추정
const costEstimate = await CostEstimator.estimate({
template: input.template,
database: input.options?.database,
messaging: input.options?.messaging,
});
// Scaffolder 작업 실행
const task = await ScaffolderClient.createTask({
templateRef: `template:default/${input.template}-service`,
values: {
serviceName: input.name,
owner: input.owner,
system: input.system,
database: input.options?.database,
messaging: input.options?.messaging,
},
createdBy: userId,
});
// 감사 로그 기록
await AuditLogger.log({
action: 'service.create',
actor: userId,
resource: input.name,
details: {
template: input.template,
options: input.options,
estimatedMonthlyCost: costEstimate.monthly,
},
});
return res.status(202).json({
taskId: task.id,
status: 'provisioning',
estimatedTime: '5-10 minutes',
estimatedMonthlyCost: costEstimate.monthly,
statusUrl: `/api/v1/tasks/${task.id}`,
});
});
export { router as servicesRouter };내부 API의 인증은 조직의 SSO(Single Sign-On) 시스템과 통합합니다.
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../auth/oidc';
interface PlatformUser {
id: string;
email: string;
groups: string[];
roles: string[];
}
declare global {
namespace Express {
interface Request {
user?: PlatformUser;
}
}
}
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const payload = await verifyToken(token);
req.user = {
id: payload.sub,
email: payload.email,
groups: payload.groups || [],
roles: payload.roles || [],
};
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}RBAC(Role-Based Access Control) 기반의 인가 정책을 적용합니다.
roles:
developer:
permissions:
- action: "service.create"
conditions:
owner: "self-team"
- action: "infrastructure.request"
conditions:
environment: ["dev", "staging"]
estimatedCost:
lessThan: 200
- action: "service.read"
- action: "cost.read"
conditions:
scope: "own-team"
team-lead:
inherits: developer
permissions:
- action: "infrastructure.request"
conditions:
environment: ["dev", "staging", "production"]
estimatedCost:
lessThan: 1000
- action: "infrastructure.approve"
conditions:
team: "own-team"
- action: "cost.read"
conditions:
scope: "own-team"
platform-admin:
permissions:
- action: "*"모든 플랫폼 API 호출은 감사 로그에 기록되어야 합니다.
interface AuditEvent {
action: string;
actor: string | undefined;
resource: string;
details: Record<string, unknown>;
result?: 'success' | 'failure';
reason?: string;
}
export class AuditLogger {
static async log(event: AuditEvent): Promise<void> {
const auditRecord = {
timestamp: new Date().toISOString(),
...event,
metadata: {
apiVersion: 'v1',
source: 'platform-api',
},
};
// 구조화된 로그로 출력
console.log(JSON.stringify(auditRecord));
// 영구 저장소에 기록 (예: Elasticsearch)
await AuditStore.write(auditRecord);
}
}감사 로그에 기록할 주요 이벤트는 다음과 같습니다.
| 이벤트 | 설명 | 위험도 |
|---|---|---|
service.create | 새 서비스 생성 | 낮음 |
service.delete | 서비스 삭제 | 높음 |
infrastructure.request | 인프라 요청 | 중간 |
infrastructure.approve | 인프라 승인 | 중간 |
policy.override | 정책 예외 적용 | 높음 |
rbac.change | 권한 변경 | 높음 |
개발자 경험을 완성하려면 포털과 함께 CLI 도구를 제공해야 합니다. 터미널에서 작업하는 개발자에게 CLI는 가장 자연스러운 인터페이스입니다.
# 새 서비스 생성
platform create service checkout-service \
--template spring-boot \
--owner team-checkout \
--database postgresql
# 서비스 목록 조회
platform list services --team team-checkout
# 서비스 상태 확인
platform status checkout-service
# 데이터베이스 프로비저닝
platform create database checkout-db \
--engine postgresql \
--size medium \
--environment dev
# 비용 조회
platform cost checkout-service --period 30d
# 환경 전환
platform env switch staging
# 로그 조회
platform logs checkout-service --tail 100package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "platform",
Short: "내부 개발자 플랫폼 CLI",
Long: "IDP의 모든 기능을 커맨드라인에서 사용할 수 있습니다.",
}
var createServiceCmd = &cobra.Command{
Use: "service [name]",
Short: "새 서비스 생성",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
template, _ := cmd.Flags().GetString("template")
owner, _ := cmd.Flags().GetString("owner")
database, _ := cmd.Flags().GetString("database")
fmt.Printf("Creating service: %s\n", name)
fmt.Printf(" Template: %s\n", template)
fmt.Printf(" Owner: %s\n", owner)
fmt.Printf(" Database: %s\n", database)
// Platform API 호출
client := NewPlatformClient()
result, err := client.CreateService(CreateServiceInput{
Name: name,
Template: template,
Owner: owner,
Options: ServiceOptions{
Database: database,
},
})
if err != nil {
return fmt.Errorf("service creation failed: %w", err)
}
fmt.Printf("\nService creation started!\n")
fmt.Printf(" Task ID: %s\n", result.TaskID)
fmt.Printf(" Status: %s\n", result.StatusURL)
return nil
},
}
func init() {
createCmd := &cobra.Command{
Use: "create",
Short: "리소스 생성",
}
createServiceCmd.Flags().String("template", "spring-boot", "서비스 템플릿")
createServiceCmd.Flags().String("owner", "", "소유 팀")
createServiceCmd.Flags().String("database", "none", "데이터베이스 유형")
_ = createServiceCmd.MarkFlagRequired("owner")
createCmd.AddCommand(createServiceCmd)
rootCmd.AddCommand(createCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}CLI 도구는 자동 완성(Auto-completion)을 지원해야 합니다. Cobra(Go)나 oclif(TypeScript) 같은 CLI 프레임워크는 Bash, Zsh, Fish 셸의 자동 완성을 기본 지원합니다.
내부 API라 해도 버전 관리는 필수입니다. 플랫폼 API의 소비자가 여러 팀에 걸쳐 있기 때문에, 하위 호환성을 깨는 변경은 큰 영향을 미칩니다.
/api/v1/services # 현재 안정 버전
/api/v2/services # 차기 버전 (베타)
/api/v1/infrastructure # 현재 안정 버전버전 수명 주기:
| 단계 | 설명 | 기간 |
|---|---|---|
| Alpha | 내부 테스트용, 언제든 변경 가능 | 자유 |
| Beta | 외부 팀 테스트, 큰 변경 사전 공지 | 1-2개월 |
| Stable | 프로덕션 사용, 하위 호환성 보장 | 최소 6개월 |
| Deprecated | 신규 사용 중단 권고 | 3개월 유예 |
| Removed | 완전 제거 | - |
이번 장에서는 플랫폼 API의 설계와 구현을 다루었습니다.
다음 장에서는 비용 가시성과 FinOps 통합을 다룹니다. 개발자에게 비용을 보여주는 방법, Backstage 비용 대시보드, 태그 기반 비용 할당, 그리고 AI 기반 비용 최적화까지 살펴보겠습니다.
이 글이 도움이 되셨나요?
FinOps 원칙과 플랫폼 통합, Backstage 비용 대시보드, 태그 기반 비용 할당, 리소스 생성 시점의 비용 예측, 그리고 AI 기반 비용 최적화를 다룹니다.
셀프서비스의 핵심 원칙, GitOps 기반 인프라 요청, Crossplane을 활용한 Kubernetes 네이티브 인프라 추상화, 그리고 승인 워크플로우를 다룹니다.
플랫폼 팀의 구조와 Team Topologies 적용, 채택률 측정과 개선, 개발자 만족도(NPS), 이해관계자 관리, 그리고 플랫폼 성숙도 모델을 다룹니다.