Backstage 골든 패스(Golden Path) 템플릿: 보안과 CI/CD가 내장된 서비스를 처음부터 만드는 법
새 프로젝트를 시작할 때마다 비슷한 일이 반복됩니다. CI/CD 설정, Dockerfile 작성, 보안 스캔 연동, 모니터링 대시보드 구성… "이거 저번에도 했던 거 아닌가?" 싶지만, 새 저장소를 만들 때마다 누군가의 예전 프로젝트를 복사하거나, 위키를 뒤지거나, 팀 채널에 "Node.js 보일러플레이트 어디 있어요?"를 묻게 됩니다. 저도 처음엔 이게 당연한 일인 줄 알았는데, Backstage의 골든 패스 개념을 접하고 나서 생각이 바뀌었습니다.
Platform Engineering.org의 대규모 Backstage 적용 사례를 보면, 팀 규모가 커질수록 이 반복 작업이 얼마나 큰 비용이 되는지 실감하게 됩니다. 팀이 10명일 때 위키로 버티던 것이, 100명이 되면 구조적으로 해결하지 않으면 안 되는 문제가 됩니다. 2025~2026년에 플랫폼 엔지니어링이 주류가 된 배경도 결국 여기 있습니다. 이 글에서는 Backstage Software Template을 직접 작성해서, 팀원 누구나 클릭 몇 번으로 보안·CI/CD·모니터링이 내장된 서비스를 생성하는 방법을 살펴봅니다.
이 글의 전제 지식: Git 워크플로와 YAML 문법에 익숙하면 대부분의 내용을 따라올 수 있습니다. "심화 패턴" 섹션(Crossplane + ArgoCD)은 Kubernetes 기본 개념과 GitOps 경험이 있는 분들을 위한 내용입니다. 프론트엔드나 주니어 백엔드 개발자라면 앞 두 예시까지만 봐도 충분합니다.
핵심 개념
골든 패스란 정확히 무엇인가
"골든 패스"라는 단어 자체는 꽤 마케팅스럽게 들립니다. 솔직히 처음 들었을 때 또 다른 유행어인가 싶었습니다. 하지만 실체는 명확합니다. 조직 내에서 검증되고 지원되는 최선의 개발 방식을 하나의 실행 가능한 템플릿으로 패키징한 것입니다.
단순한 코드 스니펫이나 README 모음이 아닙니다. 골든 패스 템플릿은 개발자가 이름 하나 입력하면 다음 것들이 한꺼번에 만들어지도록 설계됩니다.
| 구성 요소 | 자동으로 포함되는 것 |
|---|---|
| 저장소 | GitHub/GitLab 저장소 생성 + 브랜치 보호 규칙 |
| CI/CD | GitHub Actions 또는 GitLab CI 워크플로 파일 |
| 보안 | Trivy/Snyk 스캔 파이프라인 내장 |
| 배포 | Helm 차트 또는 Kustomize 매니페스트 |
| 모니터링 | Grafana 대시보드 프로비저닝 |
| 카탈로그 | Backstage 서비스 카탈로그 자동 등록 |
Spotify가 이 개념을 처음 정립하면서 "표준을 강제하는 것이 아니라, 표준을 따르는 것이 가장 쉬운 경로가 되도록 설계하는 것"이라고 설명했는데, 이 표현이 핵심을 가장 잘 담고 있습니다. 개발자에게 선택지를 빼앗는 게 아니라, 좋은 선택지를 가장 접근하기 쉽게 만드는 구조입니다.
Backstage Template의 구조
Backstage에서 골든 패스는 scaffolder.backstage.io/v1beta3 스키마의 template.yaml 파일로 정의됩니다. 처음에는 steps가 가장 복잡해 보이는데, 결국 이 파일 하나를 잘 쓰는 게 전부입니다. 세 가지 핵심 블록으로 구성됩니다.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: nodejs-service
title: Node.js 백엔드 서비스
description: 조직 표준이 적용된 Node.js 서비스를 생성합니다
spec:
owner: platform-team
type: service
# 1. Parameters: 개발자가 UI에서 입력하는 값들
parameters:
- title: 서비스 정보
required: [name, owner]
properties:
name:
type: string
title: 서비스 이름
pattern: '^[a-z][a-z0-9-]*$'
owner:
type: string
title: 소유 팀
ui:field: OwnerPicker
# 2. Steps: 실제 실행되는 액션 목록
steps:
- id: fetch
name: 코드 스캐폴딩
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
- id: publish
name: GitHub 저장소 생성
action: publish:github
input:
repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
defaultBranch: main
protectDefaultBranch: true
- id: register
name: 카탈로그 등록
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
# 3. Outputs: 완료 후 보여줄 링크들
outputs:
- id: remoteUrl
title: 저장소 URL
url: ${{ steps.publish.output.remoteUrl }}| 블록 | 역할 | 예시 |
|---|---|---|
| Parameters | UI 입력 폼 정의 | 서비스명, 언어, 오너, DB 필요 여부 |
| Steps | 순차 실행되는 스캐폴더 액션 | 코드 생성 → 저장소 생성 → 카탈로그 등록 |
| Outputs | 완료 후 링크 제공 | 저장소 URL, 파이프라인 URL, 대시보드 URL |
스켈레톤(Skeleton) 디렉토리의 역할
template.yaml이 "무엇을 할지"를 정의한다면, skeleton/ 디렉토리는 "실제 파일들"을 담습니다. 이 부분에서 저도 처음에 헷갈렸는데, 변수 문법이 template.yaml과 다릅니다. 스켈레톤 파일 안에서는 Nunjucks 문법({{ values.name }}, 달러 기호 없음)을 사용하고, template.yaml의 steps 블록 안에서는 ${{ parameters.name }} 형식을 씁니다. 두 문법을 섞어 쓰면 실행 시 오류가 납니다.
my-template/
├── template.yaml
└── skeleton/
├── catalog-info.yaml
├── .github/
│ └── workflows/
│ └── ci.yaml
├── Dockerfile
└── src/
└── index.ts# skeleton/catalog-info.yaml
# 스켈레톤 파일 안에서는 Nunjucks 문법 사용 (달러 기호 없음)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: {{ values.name }}
annotations:
github.com/project-slug: myorg/{{ values.name }}
spec:
type: service
lifecycle: production
owner: {{ values.owner }}구조를 이해했으니, 이제 실제로 쓸 수 있는 예시를 살펴보겠습니다.
실전 적용
예시 1: Node.js 서비스 — CI/CD와 보안 스캔까지 내장
실무에서 가장 많이 쓰이는 시나리오입니다. 서비스 이름을 입력하면 저장소가 생성되고, GitHub Actions CI 파이프라인과 Trivy 보안 스캔이 처음부터 활성화됩니다. 이 예시 하나만 제대로 만들어도 팀의 새 서비스 생성 과정이 눈에 띄게 달라집니다.
# template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: nodejs-golden-path
title: Node.js 서비스 (골든 패스)
spec:
owner: platform-team
type: service
parameters:
- title: 서비스 정보
required: [name, owner, description]
properties:
name:
type: string
title: 서비스 이름
description: 소문자, 숫자, 하이픈만 사용 가능합니다
pattern: '^[a-z][a-z0-9-]{2,30}$'
owner:
type: string
title: 소유 팀
ui:field: OwnerPicker
description:
type: string
title: 서비스 설명
enableDatabase:
type: boolean
title: PostgreSQL 연결이 필요한가요?
default: false
steps:
- id: fetch
name: 코드 생성
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
description: ${{ parameters.description }}
enableDatabase: ${{ parameters.enableDatabase }}
- id: publish
name: GitHub 저장소 생성
action: publish:github
input:
repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
defaultBranch: main
protectDefaultBranch: true
topics:
- nodejs
- golden-path
- id: register
name: Backstage 카탈로그 등록
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
outputs:
- id: repository
title: GitHub 저장소
url: ${{ steps.publish.output.remoteUrl }}스켈레톤 CI 파일에서 한 가지 주의할 점이 있습니다. GitHub Actions 기본 러너에는 pnpm이 설치되어 있지 않아서, pnpm/action-setup 스텝을 먼저 추가해야 합니다. 이걸 빠뜨리면 파이프라인이 즉시 실패합니다.
# skeleton/.github/workflows/ci.yaml
# 이 파일은 저장소 생성 즉시 활성화됩니다
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy 보안 스캔
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
exit-code: '1'
severity: 'CRITICAL,HIGH'| 파일 | 역할 |
|---|---|
.github/workflows/ci.yaml |
저장소 생성 즉시 CI 활성화 |
catalog-info.yaml |
Backstage 카탈로그 자동 등록 메타데이터 |
Dockerfile |
표준화된 멀티스테이지 빌드 |
추가 패턴: 커스텀 액션으로 내부 시스템 연동
기본 제공 액션만으로는 부족할 때가 있습니다. 내부 CMDB 등록, Slack 알림, 라이선스 헤더 자동 추가 같은 조직 고유의 요구사항은 커스텀 액션으로 구현할 수 있습니다. 이 패턴은 Backstage 백엔드 플러그인 구조를 이미 아는 분들을 대상으로 하며, 백엔드 등록 방법은 공식 문서에서 확인할 수 있습니다.
// packages/backend/src/plugins/scaffolder/actions/registerCmdb.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { z } from 'zod';
export const createRegisterCmdbAction = (options: { cmdbApiUrl: string }) => {
return createTemplateAction({
id: 'internal:register-cmdb',
description: '내부 CMDB에 서비스를 등록합니다',
schema: {
input: z.object({
serviceName: z.string(),
owner: z.string(),
serviceType: z.enum(['backend', 'frontend', 'data-pipeline']),
}),
output: z.object({
cmdbId: z.string(),
}),
},
async handler(ctx) {
const { serviceName, owner, serviceType } = ctx.input;
ctx.logger.info(`CMDB에 ${serviceName} 등록 중...`);
const response = await fetch(`${options.cmdbApiUrl}/services`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: serviceName, owner, type: serviceType }),
});
if (!response.ok) {
throw new Error(`CMDB 등록 실패: ${response.status} ${response.statusText}`);
}
const { id } = await response.json();
ctx.output('cmdbId', id);
ctx.logger.info(`CMDB 등록 완료: ${id}`);
},
});
};커스텀 액션을 백엔드에 등록하고 나면, template.yaml에서 다른 내장 액션과 똑같이 사용할 수 있습니다.
steps:
- id: cmdb
name: 내부 CMDB 등록
action: internal:register-cmdb
input:
serviceName: ${{ parameters.name }}
owner: ${{ parameters.owner }}
serviceType: backend
- id: notify
name: Slack 알림 발송
action: internal:notify-slack
input:
channel: '#platform-updates'
message: |
새 서비스가 생성되었습니다: *${{ parameters.name }}*
소유 팀: ${{ parameters.owner }}
저장소: ${{ steps.publish.output.remoteUrl }}
CMDB ID: ${{ steps.cmdb.output.cmdbId }}심화 패턴: Crossplane + ArgoCD 연동으로 인프라까지 자동화
팀에서 실제로 이 패턴을 적용해보면, 데이터베이스 프로비저닝까지 템플릿에 포함시키는 순간 온보딩 경험이 완전히 달라집니다. Backstage, Crossplane, ArgoCD 세 가지를 연결하면 클라우드 인프라도 자동으로 만들어집니다. Kubernetes와 GitOps에 익숙하지 않다면 이 섹션은 나중에 돌아와도 됩니다.
# skeleton/infra/database.yaml (Crossplane XRD 클레임)
# Nunjucks 조건문으로 enableDatabase가 true일 때만 파일이 생성됩니다
{% if values.enableDatabase %}
apiVersion: database.myorg.io/v1alpha1
kind: PostgreSQLInstance
metadata:
name: {{ values.name }}-db
namespace: {{ values.name }}
spec:
parameters:
storageGB: 20
version: "15"
writeConnectionSecretToRef:
name: {{ values.name }}-db-secret
{% endif %}# skeleton/argocd/app.yaml
# App of Apps 패턴: 루트 ArgoCD 앱이 이 파일을 감지해 새 Application을 등록합니다
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ values.name }}
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/{{ values.name }}
targetRevision: HEAD
path: helm
destination:
server: https://kubernetes.default.svc
namespace: {{ values.name }}
syncPolicy:
automated:
prune: true
selfHeal: true이 패턴이 동작하는 방식: 템플릿이
argocd/app.yaml을 생성하면, 루트 ArgoCD 앱이 이 파일을 감지해서 새Application리소스를 클러스터에 등록합니다(App of Apps 패턴). 이후 ArgoCD가 저장소의helm/경로를 추적해 배포를 시작하고,infra/database.yaml의 Crossplane 클레임이 있으면 실제 RDS 인스턴스까지 자동으로 프로비저닝됩니다.
Crossplane XRD(Composite Resource Definition): Kubernetes CRD를 확장해서 클라우드 리소스(RDS, GCS 버킷 등)를 Kubernetes 리소스처럼 선언적으로 관리할 수 있게 해주는 Crossplane의 핵심 개념입니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 즉각적인 생산성 | 인프라·파이프라인 설정 없이 비즈니스 로직에 바로 집중할 수 있습니다 |
| 일관된 표준 적용 | 보안 스캔, 코드 서명, 의존성 정책이 처음부터 내장됩니다 |
| 온보딩 가속화 | 신규 팀원이 조직 관행을 별도 학습 없이도 표준 준수 서비스를 만들 수 있습니다 |
| 거버넌스 자동화 | 감사·컴플라이언스 요구사항이 템플릿 레벨에서 충족됩니다 |
| 카탈로그 일관성 | 모든 서비스가 Backstage 카탈로그에 자동 등록되어 조직 전체 가시성이 확보됩니다 |
단점 및 주의사항
골든 패스 vs. 황금 우리(Golden Cage): 골든 패스는 좋은 선택지를 쉽게 만드는 구조입니다. 반대로 모든 것을 강제해서 개발자가 벗어날 수 없게 만들면 "황금 우리"가 됩니다. 탈출구(escape hatch)를 문서화해두는 것이 핵심입니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 황금 우리 위험 | 엄격한 템플릿이 혁신을 막을 수 있습니다 | 탈출구 조건과 절차를 문서화합니다 |
| 드리프트(Drift) | 생성된 서비스와 최신 템플릿 사이 괴리가 커집니다 | 드리프트 감지 파이프라인을 구축하고 업데이트 PR을 자동 제안합니다 |
| 템플릿 부패 | 구식 의존성이 포함되면 신뢰도가 급락합니다 | 분기별 정기 감사 일정을 확보하고 템플릿 자체를 CI로 테스트합니다 |
| 초기 투자 비용 | 좋은 첫 번째 템플릿은 상당한 플랫폼 팀 시간이 필요합니다 | 범위를 좁게 잡고 빠르게 첫 버전을 배포한 뒤 피드백 기반으로 확장합니다 |
| 과도한 일반화 | optional 파라미터 남발은 기술 부채를 뒤로 미룹니다 |
유스케이스 경계를 명확히 하고, 다른 케이스는 별도 템플릿으로 분리합니다 |
드리프트는 특히 장기적으로 관리가 까다롭습니다. 마치 iOS 업데이트를 계속 미룬 기기처럼, 과거에 생성된 서비스들은 보안 패치나 새 표준을 자동으로 적용받지 못합니다. 이걸 처음에 고려하지 않으면 수십 개 서비스가 생긴 뒤에야 문제가 보이기 시작합니다. 처음부터 드리프트 감지 파이프라인 자리를 예약해두는 것을 권장합니다.
실무에서 가장 흔한 실수
-
첫 번째 템플릿을 너무 완벽하게 만들려는 것. 모든 경우를 커버하려다 아무도 쓰지 않는 거대한 템플릿이 됩니다. 가장 흔한 유스케이스 하나만 골라 빠르게 배포하고, 실제 피드백을 받으며 확장하는 방식이 훨씬 효과적입니다.
-
템플릿을 만들고 관리 주체를 정하지 않는 것. 플랫폼 팀이 명확히 오너십을 갖지 않으면, 아무도 업데이트하지 않아 템플릿이 빠르게 낡아버립니다.
template.yaml의spec.owner는 단순한 메타데이터가 아닙니다. -
로컬 테스트 없이 바로 배포하는 것. 로컬 Backstage 앱을 띄운 뒤
http://localhost:3000/create에서 파라미터 입력 폼이 기대대로 동작하는지, 스켈레톤 파일에 값이 올바르게 주입되는지 미리 확인해볼 수 있습니다. 저장소가 수십 개 생성된 후에야 오타를 발견하는 경험은 생각보다 자주 발생합니다.
마치며
골든 패스 템플릿의 핵심은 "좋은 선택을 가장 쉬운 선택으로 만드는 것"이며, 그 출발점은 팀에서 가장 자주 만드는 서비스 유형 하나를 골라 template.yaml을 작성하는 것입니다.
지금 바로 시작해볼 수 있는 3단계:
-
로컬에서 Backstage 띄우기:
npx @backstage/create-app@latest로 새 Backstage 앱을 생성해볼 수 있습니다. 공식 예제 템플릿이 이미 포함되어 있어서, UI 흐름을 바로 경험해볼 수 있습니다. -
첫 번째 템플릿 범위 좁히기: 팀에서 지난 6개월간 가장 많이 만든 서비스 유형 하나를 고르고, 그 저장소에서 반복적으로 복사되는 파일 목록을 정리해보시면 좋습니다. 그 파일들이 바로
skeleton/디렉토리의 초안입니다. -
template.yaml작성 후 로컬 검증:http://localhost:3000/create에서 파라미터 입력 폼이 기대대로 동작하는지, 스켈레톤 파일에 값이 올바르게 주입되는지 확인해볼 수 있습니다. 실제 GitHub 저장소 생성은 마지막에 연결해도 늦지 않습니다.
이번 주 안에 팀 내 첫 번째 템플릿 PR을 올려보시는 건 어떨까요. 처음엔 작게 시작해도 됩니다. catalog-info.yaml과 ci.yaml 두 파일만 포함된 템플릿으로도 팀의 반응이 달라지는 걸 느낄 수 있습니다.
참고 자료
- Backstage 공식 문서 - Software Templates
- Backstage 공식 문서 - Writing Custom Actions
- 10 Tips for Better Backstage Software Templates | Red Hat Developer
- How to Implement Developer Self-Service with Backstage | Red Hat Developer
- Designing Golden Paths | Red Hat Blog
- Creating a Golden Path in Backstage: Deploying Python Apps to OpenShift | MeatyBytes
- How Golden Paths Give Developers More Time to Actually Develop | Funda Blog
- The Golden Triangle: Backstage, ArgoCD, and Crossplane | Uplatz Blog
- Golden Path vs. Golden Cage in Platform Engineering | Medium
- How to Pave Golden Paths That Actually Go Somewhere | Platform Engineering
- Key Findings from Implementing Backstage for 100k+ Developers | Platform Engineering