Backstage Golden Path Template: How to Build a Service with Built-in Security and CI/CD from Scratch
Similar routines repeat whenever I start a new project. Setting up CI/CD, writing Dockerfiles, integrating security scans, configuring monitoring dashboards… You might think, "Didn't I do this last time?", but every time I create a new repository, I end up copying someone's old project, scouring the wiki, or asking in the team channel, "Where is the Node.js boilerplate?" I also initially thought this was a given, but my perspective changed after encountering Backstage's Golden Path concept.
Looking at the large-scale Backstage application case from Platform Engineering.org (https://platformengineering.org/blog/backstage-implementations-for-more-than-100k-developers), you realize just how significant the cost of this iterative work becomes as the team grows. What was manageable with a wiki when the team was 10 becomes a structural issue that must be resolved once the team reaches 100. This is ultimately the reason why platform engineering is projected to become mainstream in 2025–2026. In this article, we will explore how to create a Backstage Software Template yourself, allowing any team member to generate a service with built-in security, CI/CD, and monitoring with just a few clicks.
Prerequisites for this article: You can follow most of the content if you are familiar with Git workflows and YAML syntax. The "Advanced Patterns" section (Crossplane + ArgoCD) is intended for those with basic Kubernetes concepts and experience with GitOps. If you are a frontend or junior backend developer, reviewing only the first two examples should be sufficient.
Key Concepts
What Exactly Is the Golden Pass
The term "Golden Pass" itself sounds quite marketing-oriented. To be honest, when I first heard it, I wondered if it was just another buzzword. However, the reality is clear. It is the best development practices, verified and supported within an organization, packaged into a single actionable template.
It is not a simple collection of code snippets or READMEs. Golden Path templates are designed so that when a developer enters just a name, the following are generated all at once.
| Components | Automatically included |
|---|---|
| Repository | Create GitHub/GitLab Repository + Branch Protection Rules |
| CI/CD | GitHub Actions or GitLab CI Workflow File |
| Security | Built-in Trivy/Snyk Scan Pipeline |
| Deployment | Helm Chart or Kustomize Manifest |
| Monitoring | Grafana Dashboard Provisioning |
| Catalog | Backstage Service Catalog Auto-registration |
When Spotify first established this concept, they explained it as "designing so that following standards becomes the easiest path, rather than forcing standards," and this expression best captures the essence. It is a structure that makes good options most accessible, rather than taking away choices from developers.
Structure of Backstage Template
In Backstage, the Golden Path is defined in the template.yaml file of the scaffolder.backstage.io/v1beta3 schema. At first, the steps seem the most complex, but ultimately, it all comes down to making good use of this single file. It consists of three core blocks.
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-]*
| Block | Role | Example |
|------|------|------|
| **Parameters** | UI Input Form Definition | Service Name, Language, Owner, DB Required |
| **Steps** | Sequential Scaffold Actions | Code Generation → Repository Creation → Catalog Registration |
| **Outputs** | Provide links after completion | Repository URL, Pipeline URL, Dashboard URL |
### The Role of the Skeleton Directory
If `template.yaml` defines "what to do," the `skeleton/` directory contains the "actual files." I was confused about this part at first as well, as the **variable syntax is different from `template.yaml`.** Inside the skeleton file, use Nunjucks syntax (`{{ values.name }}`, no dollar sign), and inside the steps block of `template.yaml`, use the `${{ parameters.name }}` format. Mixing the two syntaxes will result in a runtime error.
my-template/ ├── template.yaml └── skeleton/ ├── catalog-info.yaml ├── .github/ │ └── workflows/ │ └── ci.yaml ├── Dockerfile └── src/ └── index.ts
```yaml
# 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 }}Now that you understand the structure, let's look at some practical examples.
Practical Application
Example 1: Node.js Service — Built-in CI/CD and Security Scanning
This is the most commonly used scenario in practice. When you enter a service name, a repository is created, and the GitHub Actions CI pipeline and Trivy security scan are activated from the start. Getting this one example right makes a noticeable difference to your team's new service creation process.
# 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}
There is one thing to note regarding the skeleton CI file. Since pnpm is not installed in the default GitHub Actions runner, you must add the `pnpm/action-setup` step first. If you omit this, the pipeline will fail immediately.
```yaml
# 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'| File | Role |
|---|---|
.github/workflows/ci.yaml |
Enable CI immediately after repository creation |
catalog-info.yaml |
Backstage Catalog Auto-registration Metadata |
Dockerfile |
Standardized Multi-stage Build |
Additional Pattern: Interacting with Internal Systems via Custom Actions
There are times when built-in actions alone are insufficient. Organization-specific requirements, such as internal CMDB registration, Slack notifications, and automatic license header addition, can be implemented using custom actions. This pattern is intended for those already familiar with the Backstage backend plugin structure, and the method for backend registration can be found in the official documentation.
// 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}`);
},
});
};Once you register a custom action with the backend, you can use it in template.yaml just like other built-in actions.
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 }}Advanced Pattern: Automating Infrastructure with Crossplane + ArgoCD Integration
When you actually apply this pattern to your team, the onboarding experience changes completely the moment you include database provisioning in the template. Connecting Backstage, Crossplane, and ArgoCD automatically creates cloud infrastructure as well. If you are not familiar with Kubernetes and GitOps, you can return to this section later.
# 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: trueHow this pattern works: When the template creates argocd/app.yaml, the root ArgoCD app detects this file and registers the new Application resource to the cluster (App of Apps pattern). ArgoCD then tracks the helm/ path in the repository to start the deployment, and if there is a Crossplane claim for infra/database.yaml, it automatically provisions an actual RDS instance.
Crossplane XRD (Composite Resource Definition): This is a core concept of Crossplane that extends Kubernetes CRDs to allow cloud resources (RDS, GCS buckets, etc.) to be managed declaratively like Kubernetes resources.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Instant Productivity | You can focus immediately on business logic without setting up infrastructure or pipelines |
| Consistent Standards Application | Security scanning, code signing, and dependency policies are built-in from the start |
| Accelerate Onboarding | New team members can build standards-compliant services without separate learning of organizational practices |
| Governance Automation | Audit and compliance requirements are met at the template level |
| Catalog Consistency | All services are automatically added to the Backstage catalog, ensuring visibility across the entire organization |
Disadvantages and Precautions
Golden Path vs. Golden Cage: The Golden Path is a structure that makes good choices easy to implement. Conversely, if you force everything upon the developer so they cannot escape, it becomes a "Golden Cage." The key is to document the escape hatch.
| Item | Content | Response Plan |
|---|---|---|
| Golden Cage Dangers | Rigid templates can stifle innovation | Document exit conditions and procedures |
| Drift | The discrepancy between the generated service and the latest template widens | Build a drift detection pipeline and automatically suggest update PRs |
| Template Corruption | Reliability plummets if outdated dependencies are included | Secure a regular quarterly audit schedule and test the templates themselves with CI |
| Initial Investment Costs | A good first template requires significant platform team time | Keep the scope narrow, deploy the first version quickly, and scale based on feedback |
| Overgeneralization | optional Parameter overuse defers technical debt |
Clearly define use case boundaries and separate other cases into separate templates |
Drift is particularly tricky to manage over the long term. Much like a device that has continuously delayed iOS updates, services created in the past do not automatically receive security patches or new standards. If this is not considered from the start, problems only become apparent after dozens of services have been launched. It is recommended to reserve a spot in the drift detection pipeline from the beginning.
The Most Common Mistakes in Practice
-
Trying to make the first template too perfect. Trying to cover every case results in a massive template that no one uses. It is much more effective to select just one of the most common use cases, deploy it quickly, and scale it up based on actual feedback.
-
Creating templates without designating a management entity. If the platform team does not clearly take ownership, no one updates them, and the templates quickly become obsolete.
spec.ownerintemplate.yamlis not just simple metadata. -
Deploying immediately without local testing. After launching the local Backstage app, you can check in advance in
http://localhost:3000/createwhether the parameter input form works as expected and whether values are correctly injected into the skeleton file. It happens more often than you think to discover a typo only after dozens of repositories have been created.
In Conclusion
The core of the Golden Path Template is "making a good choice the easiest choice," and the starting point is to select one of the service types your team creates most frequently and write a template.yaml.
3 Steps to Start Right Now:
-
Launching Backstage Locally: You can create a new Backstage app with
npx @backstage/create-app@latest. An official example template is already included, so you can experience the UI flow right away. -
Narrowing the scope of the first template: Choose one service type that your team has created the most over the past six months and organize the list of files that are repeatedly copied in that repository. Those files are the drafts of the
skeleton/directory. -
Local verification after creating
template.yaml: Inhttp://localhost:3000/create, you can check if the parameter input form works as expected and if values are correctly injected into the skeleton file. It is not too late to connect the actual GitHub repository creation at the end.
Why not try submitting your team's first template PR this week? It is okay to start small. You can feel the difference in the team's reaction even with a template containing only the two files, catalog-info.yaml and ci.yaml.
Reference Materials
-
10 Tips for Better Backstage Software Templates | Red Hat Developer
-
How to Implement Developer Self-Service with Backstage | Red Hat Developer
-
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 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 }}
-
There is one thing to note regarding the skeleton CI file. Since pnpm is not installed in the default GitHub Actions runner, you must add the `pnpm/action-setup` step first. If you omit this, the pipeline will fail immediately.
```yaml
# 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'| File | Role |
|---|---|
.github/workflows/ci.yaml |
Enable CI immediately after repository creation |
catalog-info.yaml |
Backstage Catalog Auto-registration Metadata |
Dockerfile |
Standardized Multi-stage Build |
Additional Pattern: Interacting with Internal Systems via Custom Actions
There are times when built-in actions alone are insufficient. Organization-specific requirements, such as internal CMDB registration, Slack notifications, and automatic license header addition, can be implemented using custom actions. This pattern is intended for those already familiar with the Backstage backend plugin structure, and the method for backend registration can be found in the official documentation.
// 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}`);
},
});
};Once you register a custom action with the backend, you can use it in template.yaml just like other built-in actions.
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 }}Advanced Pattern: Automating Infrastructure with Crossplane + ArgoCD Integration
When you actually apply this pattern to your team, the onboarding experience changes completely the moment you include database provisioning in the template. Connecting Backstage, Crossplane, and ArgoCD automatically creates cloud infrastructure as well. If you are not familiar with Kubernetes and GitOps, you can return to this section later.
# 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: trueHow this pattern works: When the template creates argocd/app.yaml, the root ArgoCD app detects this file and registers the new Application resource to the cluster (App of Apps pattern). ArgoCD then tracks the helm/ path in the repository to start the deployment, and if there is a Crossplane claim for infra/database.yaml, it automatically provisions an actual RDS instance.
Crossplane XRD (Composite Resource Definition): This is a core concept of Crossplane that extends Kubernetes CRDs to allow cloud resources (RDS, GCS buckets, etc.) to be managed declaratively like Kubernetes resources.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Instant Productivity | You can focus immediately on business logic without setting up infrastructure or pipelines |
| Consistent Standards Application | Security scanning, code signing, and dependency policies are built-in from the start |
| Accelerate Onboarding | New team members can build standards-compliant services without separate learning of organizational practices |
| Governance Automation | Audit and compliance requirements are met at the template level |
| Catalog Consistency | All services are automatically added to the Backstage catalog, ensuring visibility across the entire organization |
Disadvantages and Precautions
Golden Path vs. Golden Cage: The Golden Path is a structure that makes good choices easy to implement. Conversely, if you force everything upon the developer so they cannot escape, it becomes a "Golden Cage." The key is to document the escape hatch.
| Item | Content | Response Plan |
|---|---|---|
| Golden Cage Dangers | Rigid templates can stifle innovation | Document exit conditions and procedures |
| Drift | The discrepancy between the generated service and the latest template widens | Build a drift detection pipeline and automatically suggest update PRs |
| Template Corruption | Reliability plummets if outdated dependencies are included | Secure a regular quarterly audit schedule and test the templates themselves with CI |
| Initial Investment Costs | A good first template requires significant platform team time | Keep the scope narrow, deploy the first version quickly, and scale based on feedback |
| Overgeneralization | optional Parameter overuse defers technical debt |
Clearly define use case boundaries and separate other cases into separate templates |
Drift is particularly tricky to manage over the long term. Much like a device that has continuously delayed iOS updates, services created in the past do not automatically receive security patches or new standards. If this is not considered from the start, problems only become apparent after dozens of services have been launched. It is recommended to reserve a spot in the drift detection pipeline from the beginning.
The Most Common Mistakes in Practice
-
Trying to make the first template too perfect. Trying to cover every case results in a massive template that no one uses. It is much more effective to select just one of the most common use cases, deploy it quickly, and scale it up based on actual feedback.
-
Creating templates without designating a management entity. If the platform team does not clearly take ownership, no one updates them, and the templates quickly become obsolete.
spec.ownerintemplate.yamlis not just simple metadata. -
Deploying immediately without local testing. After launching the local Backstage app, you can check in advance in
http://localhost:3000/createwhether the parameter input form works as expected and whether values are correctly injected into the skeleton file. It happens more often than you think to discover a typo only after dozens of repositories have been created.
In Conclusion
The core of the Golden Path Template is "making a good choice the easiest choice," and the starting point is to select one of the service types your team creates most frequently and write a template.yaml.
3 Steps to Start Right Now:
-
Launching Backstage Locally: You can create a new Backstage app with
npx @backstage/create-app@latest. An official example template is already included, so you can experience the UI flow right away. -
Narrowing the scope of the first template: Choose one service type that your team has created the most over the past six months and organize the list of files that are repeatedly copied in that repository. Those files are the drafts of the
skeleton/directory. -
Local verification after creating
template.yaml: Inhttp://localhost:3000/create, you can check if the parameter input form works as expected and if values are correctly injected into the skeleton file. It is not too late to connect the actual GitHub repository creation at the end.
Why not try submitting your team's first template PR this week? It is okay to start small. You can feel the difference in the team's reaction even with a template containing only the two files, catalog-info.yaml and ci.yaml.
Reference Materials
-
10 Tips for Better Backstage Software Templates | Red Hat Developer
-
How to Implement Developer Self-Service with Backstage | Red Hat Developer
-
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 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 }}
-
| Block | Role | Example |
|------|------|------|
| **Parameters** | UI Input Form Definition | Service Name, Language, Owner, DB Required |
| **Steps** | Sequential Scaffold Actions | Code Generation → Repository Creation → Catalog Registration |
| **Outputs** | Provide links after completion | Repository URL, Pipeline URL, Dashboard URL |
### The Role of the Skeleton Directory
If `template.yaml` defines "what to do," the `skeleton/` directory contains the "actual files." I was confused about this part at first as well, as the **variable syntax is different from `template.yaml`.** Inside the skeleton file, use Nunjucks syntax (`{{ values.name }}`, no dollar sign), and inside the steps block of `template.yaml`, use the `${{ parameters.name }}` format. Mixing the two syntaxes will result in a runtime error.
my-template/ ├── template.yaml └── skeleton/ ├── catalog-info.yaml ├── .github/ │ └── workflows/ │ └── ci.yaml ├── Dockerfile └── src/ └── index.ts
```yaml
# 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 }}Now that you understand the structure, let's look at some practical examples.
Practical Application
Example 1: Node.js Service — Built-in CI/CD and Security Scanning
This is the most commonly used scenario in practice. When you enter a service name, a repository is created, and the GitHub Actions CI pipeline and Trivy security scan are activated from the start. Getting this one example right makes a noticeable difference to your team's new service creation process.
# 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}
There is one thing to note regarding the skeleton CI file. Since pnpm is not installed in the default GitHub Actions runner, you must add the `pnpm/action-setup` step first. If you omit this, the pipeline will fail immediately.
```yaml
# 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'| File | Role |
|---|---|
.github/workflows/ci.yaml |
Enable CI immediately after repository creation |
catalog-info.yaml |
Backstage Catalog Auto-registration Metadata |
Dockerfile |
Standardized Multi-stage Build |
Additional Pattern: Interacting with Internal Systems via Custom Actions
There are times when built-in actions alone are insufficient. Organization-specific requirements, such as internal CMDB registration, Slack notifications, and automatic license header addition, can be implemented using custom actions. This pattern is intended for those already familiar with the Backstage backend plugin structure, and the method for backend registration can be found in the official documentation.
// 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}`);
},
});
};Once you register a custom action with the backend, you can use it in template.yaml just like other built-in actions.
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 }}Advanced Pattern: Automating Infrastructure with Crossplane + ArgoCD Integration
When you actually apply this pattern to your team, the onboarding experience changes completely the moment you include database provisioning in the template. Connecting Backstage, Crossplane, and ArgoCD automatically creates cloud infrastructure as well. If you are not familiar with Kubernetes and GitOps, you can return to this section later.
# 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: trueHow this pattern works: When the template creates argocd/app.yaml, the root ArgoCD app detects this file and registers the new Application resource to the cluster (App of Apps pattern). ArgoCD then tracks the helm/ path in the repository to start the deployment, and if there is a Crossplane claim for infra/database.yaml, it automatically provisions an actual RDS instance.
Crossplane XRD (Composite Resource Definition): This is a core concept of Crossplane that extends Kubernetes CRDs to allow cloud resources (RDS, GCS buckets, etc.) to be managed declaratively like Kubernetes resources.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Instant Productivity | You can focus immediately on business logic without setting up infrastructure or pipelines |
| Consistent Standards Application | Security scanning, code signing, and dependency policies are built-in from the start |
| Accelerate Onboarding | New team members can build standards-compliant services without separate learning of organizational practices |
| Governance Automation | Audit and compliance requirements are met at the template level |
| Catalog Consistency | All services are automatically added to the Backstage catalog, ensuring visibility across the entire organization |
Disadvantages and Precautions
Golden Path vs. Golden Cage: The Golden Path is a structure that makes good choices easy to implement. Conversely, if you force everything upon the developer so they cannot escape, it becomes a "Golden Cage." The key is to document the escape hatch.
| Item | Content | Response Plan |
|---|---|---|
| Golden Cage Dangers | Rigid templates can stifle innovation | Document exit conditions and procedures |
| Drift | The discrepancy between the generated service and the latest template widens | Build a drift detection pipeline and automatically suggest update PRs |
| Template Corruption | Reliability plummets if outdated dependencies are included | Secure a regular quarterly audit schedule and test the templates themselves with CI |
| Initial Investment Costs | A good first template requires significant platform team time | Keep the scope narrow, deploy the first version quickly, and scale based on feedback |
| Overgeneralization | optional Parameter overuse defers technical debt |
Clearly define use case boundaries and separate other cases into separate templates |
Drift is particularly tricky to manage over the long term. Much like a device that has continuously delayed iOS updates, services created in the past do not automatically receive security patches or new standards. If this is not considered from the start, problems only become apparent after dozens of services have been launched. It is recommended to reserve a spot in the drift detection pipeline from the beginning.
The Most Common Mistakes in Practice
-
Trying to make the first template too perfect. Trying to cover every case results in a massive template that no one uses. It is much more effective to select just one of the most common use cases, deploy it quickly, and scale it up based on actual feedback.
-
Creating templates without designating a management entity. If the platform team does not clearly take ownership, no one updates them, and the templates quickly become obsolete.
spec.ownerintemplate.yamlis not just simple metadata. -
Deploying immediately without local testing. After launching the local Backstage app, you can check in advance in
http://localhost:3000/createwhether the parameter input form works as expected and whether values are correctly injected into the skeleton file. It happens more often than you think to discover a typo only after dozens of repositories have been created.
In Conclusion
The core of the Golden Path Template is "making a good choice the easiest choice," and the starting point is to select one of the service types your team creates most frequently and write a template.yaml.
3 Steps to Start Right Now:
-
Launching Backstage Locally: You can create a new Backstage app with
npx @backstage/create-app@latest. An official example template is already included, so you can experience the UI flow right away. -
Narrowing the scope of the first template: Choose one service type that your team has created the most over the past six months and organize the list of files that are repeatedly copied in that repository. Those files are the drafts of the
skeleton/directory. -
Local verification after creating
template.yaml: Inhttp://localhost:3000/create, you can check if the parameter input form works as expected and if values are correctly injected into the skeleton file. It is not too late to connect the actual GitHub repository creation at the end.
Why not try submitting your team's first template PR this week? It is okay to start small. You can feel the difference in the team's reaction even with a template containing only the two files, catalog-info.yaml and ci.yaml.
Reference Materials
-
10 Tips for Better Backstage Software Templates | Red Hat Developer
-
How to Implement Developer Self-Service with Backstage | Red Hat Developer
-
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 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 }}
-
There is one thing to note regarding the skeleton CI file. Since pnpm is not installed in the default GitHub Actions runner, you must add the `pnpm/action-setup` step first. If you omit this, the pipeline will fail immediately.
```yaml
# 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'| File | Role |
|---|---|
.github/workflows/ci.yaml |
Enable CI immediately after repository creation |
catalog-info.yaml |
Backstage Catalog Auto-registration Metadata |
Dockerfile |
Standardized Multi-stage Build |
Additional Pattern: Interacting with Internal Systems via Custom Actions
There are times when built-in actions alone are insufficient. Organization-specific requirements, such as internal CMDB registration, Slack notifications, and automatic license header addition, can be implemented using custom actions. This pattern is intended for those already familiar with the Backstage backend plugin structure, and the method for backend registration can be found in the official documentation.
// 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}`);
},
});
};Once you register a custom action with the backend, you can use it in template.yaml just like other built-in actions.
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 }}Advanced Pattern: Automating Infrastructure with Crossplane + ArgoCD Integration
When you actually apply this pattern to your team, the onboarding experience changes completely the moment you include database provisioning in the template. Connecting Backstage, Crossplane, and ArgoCD automatically creates cloud infrastructure as well. If you are not familiar with Kubernetes and GitOps, you can return to this section later.
# 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: trueHow this pattern works: When the template creates argocd/app.yaml, the root ArgoCD app detects this file and registers the new Application resource to the cluster (App of Apps pattern). ArgoCD then tracks the helm/ path in the repository to start the deployment, and if there is a Crossplane claim for infra/database.yaml, it automatically provisions an actual RDS instance.
Crossplane XRD (Composite Resource Definition): This is a core concept of Crossplane that extends Kubernetes CRDs to allow cloud resources (RDS, GCS buckets, etc.) to be managed declaratively like Kubernetes resources.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Instant Productivity | You can focus immediately on business logic without setting up infrastructure or pipelines |
| Consistent Standards Application | Security scanning, code signing, and dependency policies are built-in from the start |
| Accelerate Onboarding | New team members can build standards-compliant services without separate learning of organizational practices |
| Governance Automation | Audit and compliance requirements are met at the template level |
| Catalog Consistency | All services are automatically added to the Backstage catalog, ensuring visibility across the entire organization |
Disadvantages and Precautions
Golden Path vs. Golden Cage: The Golden Path is a structure that makes good choices easy to implement. Conversely, if you force everything upon the developer so they cannot escape, it becomes a "Golden Cage." The key is to document the escape hatch.
| Item | Content | Response Plan |
|---|---|---|
| Golden Cage Dangers | Rigid templates can stifle innovation | Document exit conditions and procedures |
| Drift | The discrepancy between the generated service and the latest template widens | Build a drift detection pipeline and automatically suggest update PRs |
| Template Corruption | Reliability plummets if outdated dependencies are included | Secure a regular quarterly audit schedule and test the templates themselves with CI |
| Initial Investment Costs | A good first template requires significant platform team time | Keep the scope narrow, deploy the first version quickly, and scale based on feedback |
| Overgeneralization | optional Parameter overuse defers technical debt |
Clearly define use case boundaries and separate other cases into separate templates |
Drift is particularly tricky to manage over the long term. Much like a device that has continuously delayed iOS updates, services created in the past do not automatically receive security patches or new standards. If this is not considered from the start, problems only become apparent after dozens of services have been launched. It is recommended to reserve a spot in the drift detection pipeline from the beginning.
The Most Common Mistakes in Practice
-
Trying to make the first template too perfect. Trying to cover every case results in a massive template that no one uses. It is much more effective to select just one of the most common use cases, deploy it quickly, and scale it up based on actual feedback.
-
Creating templates without designating a management entity. If the platform team does not clearly take ownership, no one updates them, and the templates quickly become obsolete.
spec.ownerintemplate.yamlis not just simple metadata. -
Deploying immediately without local testing. After launching the local Backstage app, you can check in advance in
http://localhost:3000/createwhether the parameter input form works as expected and whether values are correctly injected into the skeleton file. It happens more often than you think to discover a typo only after dozens of repositories have been created.
In Conclusion
The core of the Golden Path Template is "making a good choice the easiest choice," and the starting point is to select one of the service types your team creates most frequently and write a template.yaml.
3 Steps to Start Right Now:
-
Launching Backstage Locally: You can create a new Backstage app with
npx @backstage/create-app@latest. An official example template is already included, so you can experience the UI flow right away. -
Narrowing the scope of the first template: Choose one service type that your team has created the most over the past six months and organize the list of files that are repeatedly copied in that repository. Those files are the drafts of the
skeleton/directory. -
Local verification after creating
template.yaml: Inhttp://localhost:3000/create, you can check if the parameter input form works as expected and if values are correctly injected into the skeleton file. It is not too late to connect the actual GitHub repository creation at the end.
Why not try submitting your team's first template PR this week? It is okay to start small. You can feel the difference in the team's reaction even with a template containing only the two files, catalog-info.yaml and ci.yaml.
Reference Materials
- Backstage Official Documentation - Software Templates
- Backstage Official Documentation - 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