Backstage, ArgoCD, Crossplane으로 엔드투엔드 IDP를 구축하는 실전 프로젝트. Golden Path 작성, 셀프서비스 워크플로우, 비용 가시성 통합까지 전 과정을 다룹니다.
이 실전 프로젝트에서는 지금까지 배운 모든 개념을 종합하여 하나의 완전한 IDP를 구축합니다.
# Kubernetes 클러스터 (EKS 또는 로컬 Kind)
kubectl version --client
# Helm
helm version
# Node.js 20+
node --version
# Yarn 4+
yarn --version
# GitHub 계정 + Personal Access Token
# AWS 계정 (Crossplane용)npx @backstage/create-app@latest --name my-idp
cd my-idpapp:
title: "My Company IDP"
baseUrl: http://localhost:3000
organization:
name: "My Company"
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
database:
client: pg
connection:
host: localhost
port: 5432
user: backstage
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
- Domain
- Group
- User
- Template
providers:
github:
myOrg:
organization: ${GITHUB_ORG}
catalogPath: /catalog-info.yaml
filters:
branch: main
schedule:
frequency:
minutes: 30
timeout:
minutes: 3
locations:
- type: file
target: ../../templates/spring-boot-service/template.yaml
rules:
- allow: [Template]
- type: file
target: ../../templates/go-grpc-service/template.yaml
rules:
- allow: [Template]
- type: file
target: ../../org/org-data.yaml
techdocs:
builder: local
generator:
runIn: local
publisher:
type: localapiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: org-data
spec:
targets:
- ./groups.yaml
- ./users.yaml
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: platform-team
description: "Platform Engineering Team"
spec:
type: team
profile:
displayName: "Platform Team"
children: []
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: checkout-team
description: "Checkout Service Team"
spec:
type: team
profile:
displayName: "Checkout Team"
children: []
---
apiVersion: backstage.io/v1alpha1
kind: Domain
metadata:
name: payment
description: "결제 도메인"
spec:
owner: group:default/checkout-team
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: checkout-system
description: "결제 처리 시스템"
spec:
owner: group:default/checkout-team
domain: paymentapiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: spring-boot-service
title: "Spring Boot 마이크로서비스"
description: "Spring Boot 기반 마이크로서비스를 생성합니다. CI/CD, 모니터링, Kubernetes 배포가 사전 구성됩니다."
tags:
- java
- spring-boot
- recommended
spec:
owner: group:default/platform-team
type: service
parameters:
- title: "서비스 기본 정보"
required:
- serviceName
- description
- owner
properties:
serviceName:
title: "서비스 이름"
type: string
description: "소문자, 하이픈 구분"
pattern: "^[a-z][a-z0-9-]*$"
ui:autofocus: true
description:
title: "설명"
type: string
owner:
title: "소유 팀"
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
system:
title: "시스템"
type: string
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: System
- title: "기술 옵션"
properties:
database:
title: "데이터베이스"
type: string
enum: ["postgresql", "mysql", "none"]
default: "none"
steps:
- id: fetch
name: "프로젝트 스켈레톤 생성"
action: fetch:template
input:
url: ./skeleton
values:
serviceName: ${{ parameters.serviceName }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
system: ${{ parameters.system }}
database: ${{ parameters.database }}
- id: publish
name: "GitHub 저장소 생성"
action: publish:github
input:
repoUrl: "github.com?owner=${GITHUB_ORG}&repo=${{ parameters.serviceName }}"
description: ${{ parameters.description }}
defaultBranch: main
repoVisibility: internal
- id: register
name: "카탈로그 등록"
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: "GitHub 저장소"
url: ${{ steps.publish.output.remoteUrl }}
- title: "카탈로그"
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.serviceName }}
description: "${{ values.description }}"
annotations:
github.com/project-slug: "${GITHUB_ORG}/${{ values.serviceName }}"
backstage.io/techdocs-ref: dir:.
backstage.io/kubernetes-id: ${{ values.serviceName }}
tags:
- java
- spring-boot
spec:
type: service
lifecycle: production
owner: ${{ values.owner }}
system: ${{ values.system }}package com.mycompany;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}package com.mycompany.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}
}name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Build and Test
run: ./gradlew build test
- name: Build Docker Image
if: github.ref == 'refs/heads/main'
run: |
docker build -t ghcr.io/${GITHUB_ORG}/${{ values.serviceName }}:${{ github.sha }} .
docker push ghcr.io/${GITHUB_ORG}/${{ values.serviceName }}:${{ github.sha }}FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]kubectl create namespace argocd
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
--namespace argocd \
--set server.extraArgs="{--insecure}" \
--set configs.params."server\.insecure"=true새 서비스가 생성되면 자동으로 ArgoCD Application이 생성되도록 ApplicationSet을 구성합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: services
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/mycompany/infra-gitops.git
revision: main
directories:
- path: "apps/*"
template:
metadata:
name: "{{path.basename}}"
spec:
project: default
source:
repoURL: "https://github.com/mycompany/{{path.basename}}.git"
targetRevision: main
path: k8s/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: "{{path.basename}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=truehelm repo add crossplane-stable https://charts.crossplane.io/stable
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace
# AWS Provider
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-rds
spec:
package: xpkg.upbound.io/upbound/provider-aws-rds:v1.15.0
EOFapiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.infra.mycompany.com
spec:
group: infra.mycompany.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: Database
plural: databases
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum: ["postgresql", "mysql"]
size:
type: string
enum: ["small", "medium", "large"]
default: "small"
storageGB:
type: integer
default: 20
minimum: 20
maximum: 500
required:
- engine
status:
type: object
properties:
endpoint:
type: string
port:
type: integerapiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws
labels:
crossplane.io/xrd: xdatabases.infra.mycompany.com
provider: aws
spec:
compositeTypeRef:
apiVersion: infra.mycompany.com/v1
kind: XDatabase
resources:
- name: rds
base:
apiVersion: rds.aws.upbound.io/v1beta2
kind: Instance
spec:
forProvider:
region: ap-northeast-2
instanceClass: db.t4g.micro
allocatedStorage: 20
engine: postgres
engineVersion: "16"
storageEncrypted: true
publiclyAccessible: false
skipFinalSnapshot: false
backupRetentionPeriod: 7
tags:
managed-by: crossplane
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.engine
toFieldPath: spec.forProvider.engine
- type: FromCompositeFieldPath
fromFieldPath: spec.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- type: FromCompositeFieldPath
fromFieldPath: spec.size
toFieldPath: spec.forProvider.instanceClass
transforms:
- type: map
map:
small: db.t4g.micro
medium: db.t4g.medium
large: db.t4g.large
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.address
toFieldPath: status.endpoint
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.port
toFieldPath: status.portapiVersion: infra.mycompany.com/v1
kind: Database
metadata:
name: checkout-db
namespace: checkout-service
spec:
engine: postgresql
size: small
storageGB: 50Crossplane Claim을 Backstage Scaffolder에서 자동으로 생성하면, 개발자는 포털에서 몇 번의 클릭만으로 데이터베이스를 프로비저닝할 수 있습니다. 이것이 셀프서비스 인프라의 완성형입니다.
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost \
--create-namespace \
--set kubecostProductConfigs.clusterName="production" \
--set kubecostProductConfigs.labelMappingConfigs.enabled=trueimport { CostInsightsApi } from '@backstage/plugin-cost-insights-common';
export class KubecostCostInsightsClient implements CostInsightsApi {
private readonly kubecostUrl: string;
constructor(kubecostUrl: string) {
this.kubecostUrl = kubecostUrl;
}
async getLastCompleteBillingDate(): Promise<string> {
const today = new Date();
today.setDate(today.getDate() - 1);
return today.toISOString().split('T')[0];
}
async getUserGroups(userId: string): Promise<any[]> {
// Backstage 카탈로그에서 사용자의 팀 조회
const response = await fetch(
`${this.kubecostUrl}/model/allocation?window=7d&aggregate=namespace`,
);
const data = await response.json();
// 팀 매핑 로직
return this.mapToGroups(data, userId);
}
async getGroupDailyCost(group: string, intervals: string): Promise<any> {
const response = await fetch(
`${this.kubecostUrl}/model/allocation?window=${intervals}&aggregate=namespace&filterNamespaces=${group}`,
);
return this.transformCostData(await response.json());
}
private mapToGroups(data: any, userId: string): any[] {
// Kubecost 데이터를 Backstage 그룹으로 매핑
return [];
}
private transformCostData(data: any): any {
// Kubecost 데이터를 Cost Insights 형식으로 변환
return {};
}
}실제 조직에 IDP를 도입할 때의 로드맵을 제시합니다.
phase_1_mvp:
preparation:
- "플랫폼 팀 편성 (최소 2명)"
- "개발자 설문조사 실시"
- "핵심 유스케이스 3개 선정"
- "Build vs Buy 의사결정"
implementation:
- "Backstage 앱 생성 및 기본 설정"
- "GitHub 통합 설정"
- "인증 프로바이더 설정"
- "소프트웨어 카탈로그 초기 데이터 등록"
- "Golden Path 템플릿 2개 작성"
- "CI/CD 파이프라인 템플릿 작성"
validation:
- "파일럿 팀 (2-3개 팀) 선정"
- "파일럿 팀 핸즈온 워크숍"
- "파일럿 팀 피드백 수집"
- "MVP 성공 기준 달성 확인"
phase_2_growth:
infrastructure:
- "Crossplane 설치 및 XRD 작성"
- "셀프서비스 DB 프로비저닝 구현"
- "ArgoCD ApplicationSet 설정"
- "환경 프로비저닝 자동화"
expansion:
- "추가 Golden Path 템플릿 작성"
- "TechDocs 통합"
- "전사 온보딩 프로그램"
- "Platform Champion 프로그램 시작"
measurement:
- "채택률 KPI 대시보드 구축"
- "첫 개발자 NPS 설문 실시"
phase_3_maturity:
finops:
- "Kubecost 설치 및 Backstage 연동"
- "태그 기반 비용 할당 체계 구축"
- "예산 알림 설정"
- "비용 최적화 추천 기능"
security:
- "정책 엔진 (OPA) 통합"
- "보안 스코어카드 구현"
- "이미지 스캐닝 자동화"
governance:
- "서비스 성숙도 스코어 구현"
- "기술 부채 추적 대시보드"
- "컴플라이언스 자동 검증"IDP 구축의 성공을 판단하기 위한 핵심 지표를 정리합니다.
| 카테고리 | 지표 | Phase 1 목표 | Phase 3 목표 |
|---|---|---|---|
| 채택 | 템플릿 사용률 | 60% | 85% |
| 채택 | 카탈로그 등록률 | 70% | 95% |
| 효율 | 첫 배포 소요 시간 | 1일 이내 | 4시간 이내 |
| 효율 | 인프라 프로비저닝 시간 | 2시간 이내 | 30분 이내 |
| 효율 | 셀프서비스 비율 | 40% | 75% |
| 만족 | 개발자 NPS | 20+ | 40+ |
| 비용 | 비용 태그 커버리지 | N/A | 95% |
| 비용 | 비용 절감률 | N/A | 20% |
지표에 집착하지 마세요. 숫자보다 중요한 것은 개발자의 실제 경험입니다. NPS가 높아도 특정 팀이 큰 불만을 가지고 있다면, 평균이 아닌 예외에 집중해야 합니다.
10장에 걸쳐 Platform Engineering의 개념부터 실전 구축까지 다루었습니다.
1장~2장에서는 Platform Engineering의 등장 배경과 IDP 설계 원칙을 다루었습니다. DevOps의 인지 부하 문제를 해결하기 위해 Platform Engineering이 등장했으며, 사용자 리서치 기반의 설계가 핵심임을 살펴보았습니다.
3장~4장에서는 Backstage를 중심으로 개발자 포털과 소프트웨어 카탈로그를 구축했습니다. 엔티티 모델, 메타데이터 표준화, 소프트웨어 템플릿이 IDP의 기반임을 확인했습니다.
5장~6장에서는 Golden Path와 셀프서비스 인프라를 다루었습니다. 강제가 아닌 매력으로 높은 채택률을 달성하는 것, 그리고 GitOps와 Crossplane으로 인프라를 자동화하는 방법을 살펴보았습니다.
7장~8장에서는 플랫폼 API와 FinOps 통합을 다루었습니다. 플랫폼을 제품으로 바라보고, 비용 가시성을 개발자 의사결정 시점에 제공하는 것의 중요성을 확인했습니다.
9장~10장에서는 조직 확장과 실전 프로젝트를 수행했습니다. Team Topologies 기반의 팀 설계, 성숙도 모델, 그리고 엔드투엔드 IDP 구축을 실습했습니다.
Platform Engineering은 기술만의 문제가 아닙니다. 조직 문화, 개발자 경험, 그리고 제품적 사고가 함께 어우러져야 성공할 수 있습니다. 이 시리즈가 여러분의 Platform Engineering 여정에 실질적인 도움이 되기를 바랍니다.
Platform Engineering은 한 번 구축하고 끝나는 것이 아닙니다. 개발자의 피드백을 지속적으로 수집하고, 기술의 변화에 맞추어 플랫폼을 진화시켜 나가세요. 최고의 플랫폼은 개발자가 "이게 없으면 어떻게 일했지?"라고 느끼는 플랫폼입니다.
이 글이 도움이 되셨나요?
플랫폼 팀의 구조와 Team Topologies 적용, 채택률 측정과 개선, 개발자 만족도(NPS), 이해관계자 관리, 그리고 플랫폼 성숙도 모델을 다룹니다.
FinOps 원칙과 플랫폼 통합, Backstage 비용 대시보드, 태그 기반 비용 할당, 리소스 생성 시점의 비용 예측, 그리고 AI 기반 비용 최적화를 다룹니다.
Platform as a Product 관점에서의 API 계층 설계, 추상화 수준 결정, 내부 API 버전닝, 인증과 인가, 감사 로깅, 그리고 CLI 도구 제공까지 다룹니다.