← 전체 글

Backstage IDP 구축기: 개발자 셀프서비스 플랫폼 만들기


환경: EKS, Backstage v1.x, GitLab Self-hosted, ArgoCD
핵심 주제: Platform Engineering, Software Catalog 자동화, Software Template, 플러그인 통합

배경 / 문제 상황

EKS 플랫폼이 어느 정도 안정화되고 나서, 다음 문제가 보였다. 인프라팀이 병목이었다.

새 서비스를 배포하려면? 인프라팀에 GitLab 프로젝트 생성 요청, CI/CD 파이프라인 설정 요청, ArgoCD Application 등록 요청, Kubernetes 네임스페이스 생성 요청… 개발자 한 명이 새 마이크로서비스를 시작하는 데 며칠이 걸렸다. 그리고 대부분의 시간은 인프라팀을 기다리는 시간이었다.

툴 파편화 문제도 있었다. GitLab에서 소스코드 확인, ArgoCD에서 배포 상태 확인, Grafana에서 메트릭 확인, 각 서비스의 문서는 어딘가에… 개발자는 하루에도 수십 번 시스템을 오가며 컨텍스트 스위칭 비용을 치렀다.

“이 서비스 담당자가 누구지?”, “이 API는 어디서 쓰이지?”, “이 서비스가 어떤 DB에 의존하지?” — 이런 질문에 빠르게 답할 수 있는 곳이 없었다. 장애 대응 시 담당자 파악에만 10분이 넘게 걸리는 상황이 반복됐다.

해결책은 명확했다: 개발자가 스스로 할 수 있는 플랫폼(IDP, Internal Developer Platform)이 필요했다.

기술 선택 배경 — 왜 Backstage인가

IDP 솔루션 후보는 여러 가지였다. 처음엔 간단한 위키나 내부 포털 정도를 생각했다. 그러나 핵심 요구사항을 나열하자 선택지가 좁혀졌다.

요구사항:

  • 소프트웨어 카탈로그: 서비스 소유자, 의존성, 상태 일원화
  • 개발자 셀프서비스: 새 서비스 생성 시 GitLab 저장소, ArgoCD Application, Kubernetes 리소스 자동 구성
  • 기존 도구 통합: ArgoCD, Grafana, Kubernetes 플러그인
  • Docs-as-Code: 문서가 코드와 함께 관리되고 자동 게시

Backstage는 Spotify가 내부 개발자 포털로 구축하고 CNCF에 기증한 오픈소스다. 위 요구사항 전부를 공식 플러그인 생태계로 지원한다. Red Hat, Spotify, American Airlines 등 수백 개 조직이 프로덕션에서 사용하는 성숙한 플랫폼이다.

결정적인 이유는 확장 가능성이다. 단순 문서 포털이나 링크 모음이 아니라, Scaffolder를 통해 실제 인프라 프로비저닝을 자동화할 수 있다. “개발자가 버튼 하나로 프로덕션 수준의 서비스 환경을 생성”하는 것이 가능하다.

Backstage 핵심 아키텍처

Backstage는 세 가지 핵심 기능과 확장 가능한 플러그인 아키텍처로 구성된다.

Software Catalog는 모든 기술 자산의 메타데이터를 중앙에서 관리한다. “살아있는 CMDB”라고 보면 된다. 각 서비스는 catalog-info.yaml을 Git 저장소에 가지고 있고, Backstage가 이를 자동으로 발견해서 카탈로그에 등록한다.

**Software Templates(Scaffolder)**는 조직의 표준이 녹아든 프로젝트 자동 생성기다. 개발자가 웹 UI에서 몇 가지 항목을 입력하면, GitLab 저장소 생성 → 기본 코드 스캐폴딩 → ArgoCD 등록 → 카탈로그 등록이 자동으로 이루어진다.

TechDocs는 Markdown 문서를 코드와 같이 Git에서 관리하고, 빌드 시 Backstage 포털에 자동 게시한다.

Plugin Architecture는 ArgoCD, Grafana, Kubernetes 등 기존 도구를 Backstage 포털에 통합하는 확장 포인트다. 개발자가 서비스 페이지 하나에서 배포 상태, 메트릭, Pod 상태를 모두 볼 수 있다.

개발자 ──→ Backstage Portal

     ┌─────────┼─────────────┐
     │         │             │
   Catalog  Scaffolder    TechDocs
     │         │
     │    ┌────┴─────────┐
     │    GitLab 저장소   ArgoCD Application
     │    (자동 생성)     (자동 등록)

  Plugins: ArgoCD, Grafana, Kubernetes, ...

구현 과정

Step 1. Backstage 앱 생성 및 로컬 개발 환경

npm install -g yarn
npx @backstage/create-app@latest

한 가지 주의사항: Node.js 버전이 정확히 맞아야 한다. Backstage는 Node.js 20.x를 요구한다. 20.0.0(정확히)은 안 되고, 20.x LTS 버전이어야 한다.

nvm install 20 --lts
nvm use 20

로컬에서 yarn start로 검증 후 Docker 이미지를 빌드해서 EKS에 배포하는 구조다.

Step 2. GitLab 인증 통합

GitLab을 SSO로 사용하려면 백엔드에 인증 공급자를 추가한다.

yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gitlab-provider

packages/backend/src/index.ts에 등록:

backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider'));

packages/app/src/App.tsx에서 로그인 페이지 설정:

components: {
  SignInPage: props => (
    <SignInPage
      {...props}
      auto
      provider={{
        id: 'gitlab-auth-provider',
        title: 'GitLab',
        message: 'Sign in using GitLab',
        apiRef: gitlabAuthApiRef,
      }}
    />
  ),
},

app-config.production.yaml:

auth:
  environment: production
  providers:
    gitlab:
      production:
        clientId: ${AUTH_GITLAB_CLIENT_ID}
        clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
        audience: https://gitlab.example.internal
        signIn:
          resolvers:
            - resolver: usernameMatchingUserEntityName

clientIdclientSecret은 GitLab의 Application 생성 후 발급받는다.

Step 3. Software Catalog 구성

카탈로그를 수동으로 등록하는 대신, GitLab 저장소를 자동 스캔하도록 설정한다.

두 가지 Discoverer를 사용한다:

Project Discoverer: GitLab 내 모든 프로젝트에서 catalog-info.yaml을 찾아 카탈로그에 자동 등록

Org Discoverer: GitLab의 Groups/Members를 Backstage의 Group/User 엔티티로 동기화

yarn --cwd packages/backend add \
  @backstage/plugin-catalog-backend-module-gitlab \
  @backstage/plugin-catalog-backend-module-gitlab-org

app-config.yaml:

integrations:
  gitlab:
    - host: gitlab.example.internal
      apiBaseUrl: https://gitlab.example.internal/api/v4
      token: ${GITLAB_INTEGRATION_TOKEN}
      defaultBranch: main

catalog:
  rules:
    - allow: [Component, System, API, Resource, Location, Group, Domain, Template]

  providers:
    gitlab:
      # GitLab 프로젝트에서 catalog-info.yaml 자동 탐색
      project-discoverer:
        host: gitlab.example.internal
        branch: main
        skipForkedRepos: true
        includeArchivedRepos: false
        group: infra  # 특정 그룹만 스캔
        entityFilename: 'catalog-info.yaml'
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

      # GitLab 조직 구조를 Backstage Group/User로 동기화
      org-discoverer:
        host: gitlab.example.internal
        orgEnabled: true
        group: InfraTeam  # GitLab 그룹명
        relations:
          - INHERITED
          - DESCENDANTS
          - SHARED_FROM_GROUPS
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

중요: integrations.gitlab.token의 Role은 반드시 Reporter 이상이어야 한다. Developer 미만이면 카탈로그 탐색 시 API 권한 오류가 발생한다.

Step 4. Catalog 엔티티 구조 설계

Software Catalog의 핵심은 올바른 엔티티 계층을 설계하는 것이다. 의존성이 없는 순서대로 생성해야 한다.

Group → Domain → Resource
     ↘         ↗
      System → Component

실제 디렉토리 구조:

manifest/backstage/
├── 0-org/
│   └── group-app-dev-team.yaml
├── 1-domains/
│   └── domain-application.yaml
├── 2-systems/
│   └── system-main-backend.yaml
├── 3-components/
│   └── services/
│       └── service-backend-api.yaml
└── 4-resources/
    └── resource-mysql.yaml

Component 엔티티 예시:

# service-backend-api.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: service-backend-api
  description: 백엔드 핵심 비즈니스 로직 API 서버
  annotations:
    # Kubernetes 플러그인이 클러스터 리소스를 찾기 위한 ID
    backstage.io/kubernetes-id: service-backend-api
spec:
  type: service
  lifecycle: production
  owner: group:default/app-dev-team
  system: main-backend
  dependsOn:
    - resource:default/main-app-mysql

Kubernetes 플러그인 연동의 핵심: backstage.io/kubernetes-id 어노테이션을 Component에 달고, 실제 쿠버네티스 리소스에도 같은 레이블을 붙이면 Backstage가 자동으로 연결한다.

# kustomize base/kustomization.yaml
commonLabels:
  backstage.io/kubernetes-id: service-backend-api

이 두 줄만으로 Backstage Kubernetes 탭에서 해당 서비스의 Deployment, Pod, Service, Ingress 상태를 실시간으로 볼 수 있다.

Step 5. Software Template

Scaffolder가 Backstage의 핵심 가치다. 개발자가 폼을 채우면 전체 서비스 환경이 자동으로 만들어진다.

# template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: create-new-service
  title: Create a New Backend Service
  description: GitLab 저장소 생성 + 카탈로그 자동 등록
spec:
  owner: group:default/infra-team
  type: service

  parameters:
    - title: 서비스 기본 정보
      required:
        - instance_name
        - owner
      properties:
        instance_name:
          title: 서비스 이름
          type: string
          description: Kubernetes 리소스 이름 기준으로 kebab-case 권장
          ui:autofocus: true
        description:
          title: 서비스 설명
          type: string
        owner:
          title: 소유 팀
          type: string
          ui:field: OwnerPicker
          ui:options:
            allowedKinds: [Group]

    - title: GitLab 저장소 위치
      required:
        - repoUrl
      properties:
        repoUrl:
          title: 저장소 URL
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - gitlab.example.internal

    - title: 추가 옵션
      properties:
        skip_actions:
          title: GitLab 및 카탈로그 등록 건너뛰기
          type: boolean
          description: 로컬 테스트 시에만 true로 설정
          default: false

  steps:
    - id: fetch-skeleton
      name: 스켈레톤 코드 생성
      action: fetch:template
      input:
        url: ./skeleton
        values:
          instance_name: ${{ parameters.instance_name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}

    - id: publish
      name: GitLab에 저장소 생성
      if: ${{ not parameters.skip_actions }}
      action: publish:gitlab
      input:
        repoUrl: ${{ parameters.repoUrl }}
        defaultBranch: main

    - id: register
      name: 카탈로그에 등록
      if: ${{ not parameters.skip_actions }}
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'

  output:
    links:
      - title: GitLab 저장소 보기
        if: ${{ not parameters.skip_actions }}
        url: ${{ steps.publish.output.remoteUrl }}
      - title: 카탈로그에서 열기
        if: ${{ not parameters.skip_actions }}
        icon: catalog
        entityRef: ${{ steps.register.output.entityRef }}

skeleton/catalog-info.yaml은 Nunjucks 템플릿으로 작성하고, 파라미터 값이 자동으로 주입된다:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: ${{ values.instance_name }}
  description: ${{ values.description | default('No description provided.') }}
  annotations:
    gitlab.com/project-slug: '${{ values.repo_namespace }}/${{ values.repo_name }}'
spec:
  type: service
  lifecycle: experimental
  owner: ${{ values.owner }}

Step 6. 플러그인 통합

ArgoCD, Grafana, Kubernetes 플러그인을 추가하면 Backstage가 진짜 “단일 창구”가 된다. 서비스 페이지 하나에서 다음을 볼 수 있다:

  • Overview 탭: 서비스 기본 정보, 소유 팀, 소속 시스템, 의존 리소스
  • CI/CD 탭: GitLab Pipeline 상태 (가장 최근 빌드)
  • CD 탭: ArgoCD Application 상태, 마지막 배포 시각, Sync 상태
  • Kubernetes 탭: 클러스터 내 Deployment, Pod, Service 실시간 상태
  • Monitoring 탭: Grafana 대시보드 임베드
  • Docs 탭: TechDocs로 렌더링된 서비스 문서

핵심 트러블슈팅

문제 1: Scaffolder Push 401 — 봇 계정 권한

증상: Template 실행 시 GitLab에 저장소는 생성되지만 첫 커밋 Push에서 401 Unauthorized 발생.

"error": "A default branch (e.g. main) does not yet exist..."
"msg": "stopping transaction because pre-receive hook failed"

원인: GitLab 그룹에 설정된 “Protected Branch” 규칙이 기본 브랜치에는 Maintainer 이상만 Push 허용으로 설정되어 있었다. Backstage Scaffolder가 사용하는 Integration Token이 Developer 역할이어서, 비어있는 저장소에 첫 커밋(= 기본 브랜치 생성)을 Push할 수 없었다.

해결: 두 가지 방법이 있다.

첫째, Integration Token의 역할을 Maintainer로 상향한다 (간단하지만 광범위한 권한).

둘째(권장), GitLab 그룹의 Protected Branch 설정에서 Allowed to push and mergeMaintainers에서 **Developers + Maintainers**로 변경한다. 토큰에는 최소 권한을 주고, 브랜치 보호 규칙만 조정하는 보안적으로 더 나은 방법이다.

문제 2: 카탈로그 등록 400 — 브랜치 이름 불일치

증상: Scaffolder로 저장소 생성 후 카탈로그 등록 단계에서 400 Bad Request. 로그에 blob/master/catalog-info.yaml을 찾으려고 시도하는 것이 보임.

원인: Backstage의 publish:gitlab 액션이 기본 브랜치를 master로 가정했으나, GitLab 인스턴스의 기본 브랜치가 main이었다. GitLab 14 이후 버전은 기본 브랜치를 main으로 변경했기 때문에 발생하는 불일치다.

해결: app-config.yaml의 GitLab integration 설정에 defaultBranch: main을 명시한다. 한 번 설정하면 모든 템플릿에 적용된다.

integrations:
  gitlab:
    - host: gitlab.example.internal
      token: ${GITLAB_INTEGRATION_TOKEN}
      defaultBranch: main   # ← 이 줄이 없으면 master로 시도

템플릿의 publish 스텝에도 defaultBranch: main을 명시하면 이중으로 안전하다.

문제 3: GitLab Auth 404 — 설정 병합 충돌

증상: GitLab SSO 로그인 시도 시 /api/auth/gitlab/handler/frame 에서 404 반환. app-config.yaml 설정은 정상인데 에러 발생.

원인: app-config.yamlapp-config.local.yaml(또는 app-config.production.yaml)을 Backstage가 병합할 때, auth.providers.gitlab.{environment} 키의 환경 이름이 서로 달랐다. 예를 들어 한 파일에는 development, 다른 파일에는 production으로 설정되어 실제로 사용되는 설정 블록을 찾지 못한 것이다.

해결: 모든 config 파일에서 auth.environmentauth.providers.gitlab.{environment} 키를 일치시킨다. 또는 환경별 config 파일을 각각 완전하게 작성하고 병합을 최소화한다.

문제 4: Kubernetes 플러그인 메트릭 API 오류

증상: Backstage Kubernetes 탭에서 Pod 목록은 나오는데 CPU/메모리 사용량이 보이지 않음. Metrics Addon을 설치했음에도 kubectl top 실패.

원인: EKS Add-on으로 Metrics Server를 설치하면 워커 노드의 보안그룹에 10251 포트 인바운드 허용 규칙이 자동으로 추가되지 않는다. API Server에서 Metrics Server Pod로의 트래픽이 차단됐다.

해결: 워커 노드 보안그룹에 클러스터 보안그룹을 소스로 10251 포트 인바운드 규칙을 수동으로 추가한다. Terraform으로 EKS를 관리한다면 보안그룹 규칙에 이 항목을 명시적으로 추가해야 한다.

문제 5: CSP로 인한 GitLab 아이콘 미표시

증상: 개발 환경에서는 로그인 페이지에 GitLab 아이콘이 보이는데, 프로덕션 배포 후에는 깨진 이미지로 표시.

원인: 프로덕션 환경의 Content Security Policy(CSP)가 기본적으로 img-src 'self'만 허용하므로, GitLab 서버의 이미지 URL을 차단한다.

해결: app-config.production.yaml에 CSP 예외 추가:

backend:
  csp:
    img-src:
      - "'self'"
      - 'data:'
      - 'gitlab.example.internal'  # 사내 GitLab 도메인 추가

결과 및 개선 효과

개발자 셀프서비스 실현: 새 서비스 환경 구성 시간이 며칠 → 10분으로 단축. 인프라팀에 티켓을 올릴 필요 없이 개발자가 직접 Backstage Template을 실행하면 GitLab 저장소 + catalog-info.yaml + ArgoCD Application이 자동으로 생성된다.

서비스 소유권 가시화: 모든 서비스의 소유 팀, 담당자, 의존 DB/캐시가 카탈로그에 등록되어 있다. 장애 발생 시 “이 서비스 담당자가 누구지?” 문제가 사라졌다.

단일 창구 구현: ArgoCD, Grafana, GitLab CI 상태를 Backstage 하나에서 확인. 특히 Kubernetes 탭에서 서비스의 Pod 상태를 실시간으로 볼 수 있어 “배포됐는데 왜 안 되지?” 상황을 즉시 진단할 수 있다.

GitOps 기반 카탈로그 자동화: 개발자가 catalog-info.yaml을 GitLab에 Push하면 30분 이내 Backstage 카탈로그에 자동 등록된다. 수동 등록 과정이 없어 카탈로그 정보가 항상 최신 상태다.

마무리 및 다음 스텝

Backstage는 설치가 끝난 순간이 아니라 계속 사용하면서 가치가 쌓이는 플랫폼이다. 초기 구축에서 배운 교훈을 요약하면:

반드시 기억할 세 가지:

  1. GitLab Integration Token 역할 확인: Reporter 이상이어야 Catalog Discoverer가 동작한다. Developer 미만이면 API 권한 오류.
  2. defaultBranch: main 명시: GitLab 14+는 기본 브랜치가 main이다. integration 설정에 이를 명시하지 않으면 Scaffolder가 master를 찾아 헤맨다.
  3. 환경 이름 일관성: app-config.yaml과 환경별 config 파일의 auth.environment 키를 반드시 통일한다.

다음 단계로는 두 가지를 계획 중이다.

첫째, Template 확장. 현재는 단순 GitLab 저장소 생성 + 카탈로그 등록이지만, 앞으로는 Kubernetes Namespace 생성, ArgoCD Application 생성, Helm 값 파일 초기화까지 Template 하나로 처리하는 완전 자동화 프로비저닝을 목표로 한다.

둘째, Scorecard 도입. Backstage의 Tech Insights 플러그인을 활용해서 각 서비스의 표준 준수 여부(Catalog 등록 여부, TechDocs 존재 여부, 보안 정책 준수 등)를 점수화하는 서비스 품질 대시보드를 만들 예정이다. 플랫폼이 “잘 닦인 길”을 제공하는 것을 넘어, 팀이 그 길을 잘 따라가고 있는지 피드백을 주는 구조다.

돌아보며

  • 티켓 기반 프로비저닝 병목이라는 문제 정의에서 출발해, 인프라를 개발자가 소비하는 셀프서비스 제품(IDP)으로 만든 Platform Mindset — 새 서비스 환경 구성 리드타임 며칠 → 10분을 실측으로 증명
  • GitLab·ArgoCD·Kubernetes·Grafana를 Software Catalog 하나로 묶어 서비스 소유권과 배포 상태를 단일 창구화한 통합 설계 능력
  • Scaffolder 봇 권한, 브랜치 전략, 환경별 설정 병합 같은 운영 세부까지 직접 디버깅하며 1인 아키텍트로 플랫폼을 완성한 실행력