본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 10장: 실전 프로젝트 -- Platform Engineering 구축
2026년 4월 4일·인프라·

10장: 실전 프로젝트 -- Platform Engineering 구축

Backstage, ArgoCD, Crossplane으로 엔드투엔드 IDP를 구축하는 실전 프로젝트. Golden Path 작성, 셀프서비스 워크플로우, 비용 가시성 통합까지 전 과정을 다룹니다.

17분1,446자10개 섹션
platform-engineeringdevopsinfrastructure
공유
platform-engineering10 / 10
12345678910
이전9장: 조직 확장과 플랫폼 팀 운영

학습 목표

  • Backstage + ArgoCD + Crossplane으로 엔드투엔드 IDP를 구축할 수 있습니다.
  • Golden Path 템플릿을 작성하고 테스트할 수 있습니다.
  • 셀프서비스 워크플로우를 구현할 수 있습니다.
  • 비용 가시성을 플랫폼에 통합할 수 있습니다.
  • 도입 로드맵과 성공 지표를 정의할 수 있습니다.

프로젝트 개요

이 실전 프로젝트에서는 지금까지 배운 모든 개념을 종합하여 하나의 완전한 IDP를 구축합니다.

목표 아키텍처

사전 요구사항

사전 요구사항
bash
# Kubernetes 클러스터 (EKS 또는 로컬 Kind)
kubectl version --client
 
# Helm
helm version
 
# Node.js 20+
node --version
 
# Yarn 4+
yarn --version
 
# GitHub 계정 + Personal Access Token
# AWS 계정 (Crossplane용)

Step 1: Backstage 설치 및 설정

앱 생성

Backstage 앱 생성
bash
npx @backstage/create-app@latest --name my-idp
 
cd my-idp

핵심 설정

app-config.yaml
yaml
app:
  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: local

조직 데이터 등록

org/org-data.yaml
yaml
apiVersion: 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: payment

Step 2: Golden Path 템플릿 작성

Spring Boot 마이크로서비스 템플릿

templates/spring-boot-service/template.yaml
yaml
apiVersion: 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 }}

스켈레톤 파일

templates/spring-boot-service/skeleton/catalog-info.yaml
yaml
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 }}
templates/spring-boot-service/skeleton/src/main/java/com/mycompany/Application.java
java
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);
    }
}
templates/spring-boot-service/skeleton/src/main/java/com/mycompany/controller/HealthController.java
java
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");
    }
}
templates/spring-boot-service/skeleton/.github/workflows/ci.yaml
yaml
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 }}
templates/spring-boot-service/skeleton/Dockerfile
dockerfile
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"]

Step 3: ArgoCD 설정 (GitOps)

ArgoCD 설치

ArgoCD 설치
bash
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 ApplicationSet

새 서비스가 생성되면 자동으로 ArgoCD Application이 생성되도록 ApplicationSet을 구성합니다.

argocd/applicationset.yaml
yaml
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=true

Step 4: Crossplane 인프라 추상화

Crossplane 설치 및 AWS Provider

Crossplane 설치
bash
helm 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
EOF

Database XRD

crossplane/xrd-database.yaml
yaml
apiVersion: 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: integer

Database Composition

crossplane/composition-database.yaml
yaml
apiVersion: 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.port

개발자의 DB 요청

database-claim-example.yaml
yaml
apiVersion: infra.mycompany.com/v1
kind: Database
metadata:
  name: checkout-db
  namespace: checkout-service
spec:
  engine: postgresql
  size: small
  storageGB: 50
Info

Crossplane Claim을 Backstage Scaffolder에서 자동으로 생성하면, 개발자는 포털에서 몇 번의 클릭만으로 데이터베이스를 프로비저닝할 수 있습니다. 이것이 셀프서비스 인프라의 완성형입니다.


Step 5: 비용 가시성 통합

Kubecost 설치

Kubecost 설치
bash
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=true

Backstage Cost Insights 연동

packages/backend/src/plugins/cost-insights.ts
typescript
import { 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를 도입할 때의 로드맵을 제시합니다.

단계별 체크리스트

deployment-checklist.yaml
yaml
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%
만족개발자 NPS20+40+
비용비용 태그 커버리지N/A95%
비용비용 절감률N/A20%
Warning

지표에 집착하지 마세요. 숫자보다 중요한 것은 개발자의 실제 경험입니다. 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 여정에 실질적인 도움이 되기를 바랍니다.

Tip

Platform Engineering은 한 번 구축하고 끝나는 것이 아닙니다. 개발자의 피드백을 지속적으로 수집하고, 기술의 변화에 맞추어 플랫폼을 진화시켜 나가세요. 최고의 플랫폼은 개발자가 "이게 없으면 어떻게 일했지?"라고 느끼는 플랫폼입니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#platform-engineering#devops#infrastructure

관련 글

인프라

9장: 조직 확장과 플랫폼 팀 운영

플랫폼 팀의 구조와 Team Topologies 적용, 채택률 측정과 개선, 개발자 만족도(NPS), 이해관계자 관리, 그리고 플랫폼 성숙도 모델을 다룹니다.

2026년 4월 2일·22분
인프라

8장: 비용 가시성과 FinOps 통합

FinOps 원칙과 플랫폼 통합, Backstage 비용 대시보드, 태그 기반 비용 할당, 리소스 생성 시점의 비용 예측, 그리고 AI 기반 비용 최적화를 다룹니다.

2026년 3월 31일·16분
인프라

7장: 플랫폼 API 설계

Platform as a Product 관점에서의 API 계층 설계, 추상화 수준 결정, 내부 API 버전닝, 인증과 인가, 감사 로깅, 그리고 CLI 도구 제공까지 다룹니다.

2026년 3월 29일·14분
이전 글9장: 조직 확장과 플랫폼 팀 운영

댓글

목차

약 17분 남음
  • 학습 목표
  • 프로젝트 개요
    • 목표 아키텍처
    • 사전 요구사항
  • Step 1: Backstage 설치 및 설정
    • 앱 생성
    • 핵심 설정
    • 조직 데이터 등록
  • Step 2: Golden Path 템플릿 작성
    • Spring Boot 마이크로서비스 템플릿
    • 스켈레톤 파일
  • Step 3: ArgoCD 설정 (GitOps)
    • ArgoCD 설치
    • ArgoCD ApplicationSet
  • Step 4: Crossplane 인프라 추상화
    • Crossplane 설치 및 AWS Provider
    • Database XRD
    • Database Composition
    • 개발자의 DB 요청
  • Step 5: 비용 가시성 통합
    • Kubecost 설치
    • Backstage Cost Insights 연동
  • 도입 로드맵
    • 단계별 체크리스트
  • 성공 지표
  • 시리즈를 마치며