03. Backstage Setup and Configuration Guide
Prerequisites
System Requirements
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Storage | 20 GB | 50+ GB |
| Node.js | 18.x LTS | 20.x LTS |
| Yarn | 1.22+ | 3.x+ |
Required Tools
# Node.js and Yarn
node --version # v18.x or v20.x
yarn --version # 1.22+
# Docker (for local development)
docker --version # 20.10+
# Git
git --version # 2.x+
# kubectl (for Kubernetes deployment)
kubectl version --client # 1.28+
# Optional: Helm
helm version # 3.x+
External Dependencies
- PostgreSQL 12+ (for production)
- Redis (for caching, optional but recommended)
- Git repository hosting (GitHub, GitLab, Bitbucket)
- Identity provider (LDAP, OAuth, SAML)
- Kubernetes cluster (for application deployment)
Installation Options
Option 1: Quick Start (Development)
# Install Backstage CLI
npm install -g @backstage/cli
# Create new Backstage app
npx @backstage/create-app@latest
# Follow prompts
# Enter app name: company-idp
# Select PostgreSQL as database
cd company-idp
# Start development server
yarn dev
Access Backstage at http://localhost:3000
Option 2: Production Setup
# Clone starter template
git clone https://github.com/backstage/backstage.git backstage-template
cd backstage-template
# Or create from scratch
npx @backstage/create-app@latest --skip-install
cd company-idp
# Install dependencies
yarn install
# Set up environment variables
cp .env.example .env
# Configure database
# Edit .env and app-config.yaml
# Build for production
yarn build
# Start production server
yarn start
Option 3: Docker Compose (Local Development)
# docker-compose.yaml
version: '3.8'
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: backstage
POSTGRES_PASSWORD: backstage
POSTGRES_DB: backstage
ports:
- '5432:5432'
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: always
ports:
- '6379:6379'
volumes:
- redis-data:/data
backstage:
build:
context: .
dockerfile: packages/backend/Dockerfile
restart: always
ports:
- '7007:7007'
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_USER: backstage
POSTGRES_PASSWORD: backstage
POSTGRES_DB: backstage
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
- postgres
- redis
volumes:
- ./app-config.yaml:/app/app-config.yaml
- ./app-config.production.yaml:/app/app-config.production.yaml
volumes:
postgres-data:
redis-data:
# Start services
docker-compose up -d
# View logs
docker-compose logs -f backstage
# Stop services
docker-compose down
Initial Setup
Project Structure
company-idp/
├── app-config.yaml # Main configuration
├── app-config.production.yaml # Production overrides
├── app-config.local.yaml # Local overrides (gitignored)
├── catalog-info.yaml # Catalog entities
├── package.json
├── lerna.json
├── packages/
│ ├── app/ # Frontend React app
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── apis.ts
│ │ │ ├── components/
│ │ │ └── plugins/
│ │ └── package.json
│ └── backend/ # Backend Node.js app
│ ├── src/
│ │ ├── index.ts
│ │ ├── plugins/
│ │ └── types.ts
│ ├── Dockerfile
│ └── package.json
├── plugins/ # Custom plugins
│ ├── deployment-plugin/
│ ├── traffic-management/
│ └── multi-cluster/
└── docs/ # TechDocs content
Environment Variables
# .env
NODE_ENV=development
LOG_LEVEL=debug
# Backend
BACKEND_URL=http://localhost:7007
# Database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=backstage
POSTGRES_PASSWORD=your-secure-password
POSTGRES_DB=backstage
# Redis (optional)
REDIS_HOST=localhost
REDIS_PORT=6379
# Authentication
AUTH_GITHUB_CLIENT_ID=your-github-client-id
AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
# Integrations
GITHUB_TOKEN=ghp_your_github_token
GITLAB_TOKEN=glpat_your_gitlab_token
# Argo
ARGO_WORKFLOWS_BASE_URL=https://argo-workflows.company.com
ARGO_WORKFLOWS_TOKEN=your-argo-token
ARGOCD_BASE_URL=https://argocd.company.com
ARGOCD_AUTH_TOKEN=your-argocd-token
# Kubernetes
KUBERNETES_SERVICE_ACCOUNT_TOKEN=/var/run/secrets/kubernetes.io/serviceaccount/token
Core Configuration
Main Configuration File
# app-config.yaml
app:
title: Company Internal Developer Portal
baseUrl: http://localhost:3000
organization:
name: Company Name
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
host: 0.0.0.0
csp:
connect-src: ["'self'", 'http:', 'https:']
upgrade-insecure-requests: false
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: ${POSTGRES_DB}
ssl:
require: false
rejectUnauthorized: false
cache:
store: redis
connection:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
reading:
allow:
- host: '*.company.com'
- host: 'github.com'
- host: 'gitlab.company.com'
# Integrations
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
gitlab:
- host: gitlab.company.com
token: ${GITLAB_TOKEN}
apiBaseUrl: https://gitlab.company.com/api/v4
proxy:
'/argocd/api':
target: ${ARGOCD_BASE_URL}/api/v1
changeOrigin: true
secure: true
headers:
Cookie: 'argocd.token=${ARGOCD_AUTH_TOKEN}'
'/argo-workflows':
target: ${ARGO_WORKFLOWS_BASE_URL}
changeOrigin: true
secure: true
headers:
Authorization: 'Bearer ${ARGO_WORKFLOWS_TOKEN}'
'/prometheus/api':
target: https://prometheus.company.com
changeOrigin: true
'/grafana/api':
target: https://grafana.company.com
changeOrigin: true
# Catalog configuration
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location, Domain, Group, User]
locations:
# Register organization data
- type: url
target: https://github.com/company/backstage-entities/blob/main/org.yaml
# Register all teams
- type: url
target: https://github.com/company/backstage-entities/blob/main/teams/*.yaml
# Auto-discover catalog-info.yaml in repositories
- type: url
target: https://github.com/company/*/blob/main/catalog-info.yaml
rules:
- allow: [Component, API]
providers:
github:
providerId:
organization: 'company'
catalogPath: '/catalog-info.yaml'
filters:
branch: 'main'
repository: '.*'
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
# TechDocs configuration
techdocs:
builder: 'local'
generator:
runIn: 'docker'
publisher:
type: 'local'
# Scaffolder (Software Templates)
scaffolder:
defaultAuthor:
name: Backstage IDP
email: [email protected]
defaultCommitMessage: 'Initial commit from Backstage'
# Kubernetes plugin
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: https://kubernetes.us-east-1.company.com
name: us-east-1
authProvider: 'serviceAccount'
skipTLSVerify: false
skipMetricsLookup: false
serviceAccountToken: ${K8S_US_EAST_1_TOKEN}
- url: https://kubernetes.us-west-1.company.com
name: us-west-1
authProvider: 'serviceAccount'
skipTLSVerify: false
skipMetricsLookup: false
serviceAccountToken: ${K8S_US_WEST_1_TOKEN}
- url: https://kubernetes.eu-central-1.company.com
name: eu-central-1
authProvider: 'serviceAccount'
skipTLSVerify: false
skipMetricsLookup: false
serviceAccountToken: ${K8S_EU_CENTRAL_1_TOKEN}
# Permission framework
permission:
enabled: true
# Notifications (optional)
notifications:
processors:
email:
transportConfig:
transport: smtp
hostname: smtp.company.com
port: 587
secure: true
auth:
user: ${SMTP_USER}
pass: ${SMTP_PASSWORD}
slack:
token: ${SLACK_TOKEN}
Authentication Setup
LDAP/Active Directory Integration
# app-config.yaml
auth:
environment: production
providers:
ldap:
development:
target: ldaps://ldap.company.com
bind:
dn: ${LDAP_BIND_DN}
secret: ${LDAP_BIND_SECRET}
users:
dn: 'ou=users,dc=company,dc=com'
options:
filter: '(uid={{username}})'
scope: 'sub'
attributes:
- 'uid'
- 'mail'
- 'cn'
- 'displayName'
map:
name: 'displayName'
email: 'mail'
picture: 'jpegPhoto'
groups:
dn: 'ou=groups,dc=company,dc=com'
options:
filter: '(member={{dn}})'
scope: 'sub'
attributes:
- 'cn'
- 'description'
OAuth 2.0 / OIDC
# app-config.yaml
auth:
environment: production
session:
secret: ${SESSION_SECRET}
providers:
oauth2:
development:
clientId: ${OAUTH_CLIENT_ID}
clientSecret: ${OAUTH_CLIENT_SECRET}
authorizationUrl: https://auth.company.com/oauth/authorize
tokenUrl: https://auth.company.com/oauth/token
scope: 'openid profile email groups'
# Or use OIDC
oidc:
development:
metadataUrl: https://auth.company.com/.well-known/openid-configuration
clientId: ${OIDC_CLIENT_ID}
clientSecret: ${OIDC_CLIENT_SECRET}
scope: 'openid profile email groups'
prompt: auto
GitHub OAuth (for development)
auth:
environment: development
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
signIn:
resolvers:
- resolver: emailMatchingUserEntityProfileEmail
- resolver: emailLocalPartMatchingUserEntityName
- resolver: usernameMatchingUserEntityName
Backend Authentication Setup
// packages/backend/src/plugins/auth.ts
import { createRouter } from '@backstage/plugin-auth-backend';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
discovery: env.discovery,
tokenManager: env.tokenManager,
providerFactories: {
// Custom sign-in resolver
ldap: createLdapProvider({
signIn: {
resolver: async (info, ctx) => {
const { profile } = info;
if (!profile.email) {
throw new Error('User profile does not contain an email');
}
// Look up user in catalog
const [name] = profile.email.split('@');
const entityRef = stringifyEntityRef({
kind: 'User',
namespace: DEFAULT_NAMESPACE,
name: name,
});
return ctx.signInWithCatalogUser({
entityRef,
});
},
},
}),
},
});
}
Database Configuration
PostgreSQL Setup
Installation
# Using Docker
docker run -d \
--name backstage-postgres \
-e POSTGRES_USER=backstage \
-e POSTGRES_PASSWORD=backstage \
-e POSTGRES_DB=backstage \
-p 5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
postgres:15
# Or using Helm (for Kubernetes)
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install backstage-postgres bitnami/postgresql \
--set auth.username=backstage \
--set auth.password=backstage \
--set auth.database=backstage \
--set primary.persistence.size=20Gi
Schema Initialization
# Run migrations
yarn workspace backend backstage-cli backend:bundle
yarn workspace backend knex migrate:latest
# Verify database
psql -h localhost -U backstage -d backstage -c "\dt"
Database Configuration with SSL
# app-config.production.yaml
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: 5432
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: ${POSTGRES_DB}
ssl:
require: true
rejectUnauthorized: true
ca: ${POSTGRES_SSL_CA} # Base64 encoded CA certificate
knexConfig:
pool:
min: 3
max: 12
acquireTimeoutMillis: 60000
idleTimeoutMillis: 60000
Database Backup Strategy
#!/bin/bash
# backup-database.sh
BACKUP_DIR="/backups/backstage"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backstage_$DATE.sql"
# Create backup
pg_dump -h $POSTGRES_HOST \
-U $POSTGRES_USER \
-d $POSTGRES_DB \
-F c \
-f $BACKUP_FILE
# Compress backup
gzip $BACKUP_FILE
# Upload to S3 (optional)
aws s3 cp $BACKUP_FILE.gz s3://company-backups/backstage/
# Cleanup old backups (keep 30 days)
find $BACKUP_DIR -name "backstage_*.sql.gz" -mtime +30 -delete
echo "Backup completed: $BACKUP_FILE.gz"
# CronJob for automated backups
apiVersion: batch/v1
kind: CronJob
metadata:
name: backstage-db-backup
namespace: backstage
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:15
command: ["/bin/bash", "/scripts/backup-database.sh"]
env:
- name: POSTGRES_HOST
value: backstage-postgres
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
volumeMounts:
- name: backup-script
mountPath: /scripts
- name: backups
mountPath: /backups
restartPolicy: OnFailure
volumes:
- name: backup-script
configMap:
name: backup-script
- name: backups
persistentVolumeClaim:
claimName: backstage-backups
Service Catalog Setup
Entity Model
# catalog-info.yaml - Component example
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
description: Payment processing service
annotations:
github.com/project-slug: company/payment-service
backstage.io/techdocs-ref: dir:.
argocd/app-name: payment-service-prod
grafana/dashboard-selector: 'app=payment-service'
tags:
- java
- spring-boot
- payment
links:
- url: https://payment-service.company.com
title: Production
icon: dashboard
- url: https://wiki.company.com/payment-service
title: Documentation
icon: docs
spec:
type: service
lifecycle: production
owner: team-payments
system: payment-system
dependsOn:
- resource:postgres-payment-db
- component:notification-service
providesApis:
- payment-api
consumesApis:
- fraud-detection-api
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payment-api
description: Payment processing API
spec:
type: openapi
lifecycle: production
owner: team-payments
system: payment-system
definition:
$text: https://github.com/company/payment-service/blob/main/openapi.yaml
---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: postgres-payment-db
description: PostgreSQL database for payments
spec:
type: database
owner: team-payments
system: payment-system
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: payment-system
description: Payment processing system
spec:
owner: team-payments
domain: e-commerce
---
apiVersion: backstage.io/v1alpha1
kind: Domain
metadata:
name: e-commerce
description: E-commerce domain
spec:
owner: group:engineering
Organization Structure
# org.yaml
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: engineering
description: Engineering department
spec:
type: department
children: [team-platform, team-payments, team-frontend]
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: team-platform
description: Platform Engineering Team
spec:
type: team
parent: engineering
children: []
members: [user:john.doe, user:jane.smith]
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: team-payments
description: Payments Team
spec:
type: team
parent: engineering
children: []
members: [user:alice.johnson, user:bob.wilson]
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: john.doe
description: Senior Platform Engineer
spec:
profile:
displayName: John Doe
email: [email protected]
picture: https://avatars.company.com/john.doe
memberOf: [team-platform]
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: jane.smith
description: Platform Engineer
spec:
profile:
displayName: Jane Smith
email: [email protected]
picture: https://avatars.company.com/jane.smith
memberOf: [team-platform]
Catalog Discovery
# app-config.yaml
catalog:
providers:
github:
providerId:
organization: 'company'
catalogPath: '/catalog-info.yaml'
filters:
branch: 'main'
repository: '.*'
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
ldapOrg:
default:
target: ldaps://ldap.company.com
bind:
dn: ${LDAP_BIND_DN}
secret: ${LDAP_BIND_SECRET}
users:
dn: 'ou=users,dc=company,dc=com'
options:
filter: '(objectClass=person)'
map:
name: uid
description: description
displayName: cn
email: mail
picture: jpegPhoto
memberOf: memberOf
groups:
dn: 'ou=groups,dc=company,dc=com'
options:
filter: '(objectClass=groupOfNames)'
map:
name: cn
description: description
type: groupType
displayName: cn
email: mail
memberOf: memberOf
members: member
schedule:
frequency: { hours: 1 }
timeout: { minutes: 5 }
Integration Configuration
Argo Workflows Integration
// packages/backend/src/plugins/argo-workflows.ts
import { createRouter } from '@internal/plugin-argo-workflows-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
discovery: env.discovery,
});
}
# app-config.yaml
argoWorkflows:
baseUrl: https://argo-workflows.company.com
token: ${ARGO_WORKFLOWS_TOKEN}
namespace: argo
# Workflow templates available in UI
templates:
- name: deployment-standard
displayName: Standard Deployment
description: Standard rolling deployment
parameters:
- name: app-name
required: true
- name: version
required: true
- name: environment
required: true
enum: [dev, staging, production]
- name: deployment-blue-green
displayName: Blue/Green Deployment
description: Zero-downtime blue/green deployment
parameters:
- name: app-name
required: true
- name: version
required: true
- name: environment
required: true
- name: deployment-canary
displayName: Canary Deployment
description: Progressive canary deployment
parameters:
- name: app-name
required: true
- name: version
required: true
- name: canary-percentage
required: false
default: "10"
ArgoCD Integration
# app-config.yaml
argocd:
baseUrl: https://argocd.company.com
username: ${ARGOCD_USERNAME}
password: ${ARGOCD_PASSWORD}
# Or use token
token: ${ARGOCD_AUTH_TOKEN}
# Application selector
appSelector: 'backstage-managed=true'
# Sync options
waitOptions:
timeout: 600
Kubernetes Integration
# app-config.yaml
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: https://k8s-api.us-east-1.company.com
name: us-east-1-prod
authProvider: 'serviceAccount'
serviceAccountToken: ${K8S_TOKEN_US_EAST_1}
dashboardUrl: https://k8s-dashboard.us-east-1.company.com
dashboardApp: standard
skipTLSVerify: false
caData: ${K8S_CA_US_EAST_1}
customResources:
- group: 'argoproj.io'
apiVersion: 'v1alpha1'
plural: 'rollouts'
- group: 'argoproj.io'
apiVersion: 'v1alpha1'
plural: 'applications'
Kubernetes Deployment
Namespace Setup
# kubernetes/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: backstage
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: backstage-quota
namespace: backstage
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
persistentvolumeclaims: "5"
---
apiVersion: v1
kind: LimitRange
metadata:
name: backstage-limits
namespace: backstage
spec:
limits:
- max:
cpu: "4"
memory: 8Gi
min:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 250m
memory: 256Mi
type: Container
Secrets Management
# kubernetes/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: backstage-secrets
namespace: backstage
type: Opaque
stringData:
POSTGRES_USER: backstage
POSTGRES_PASSWORD: <base64-encoded>
POSTGRES_DB: backstage
GITHUB_TOKEN: <base64-encoded>
ARGOCD_AUTH_TOKEN: <base64-encoded>
ARGO_WORKFLOWS_TOKEN: <base64-encoded>
---
# Or use External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: backstage-secrets
namespace: backstage
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: backstage-secrets
creationPolicy: Owner
data:
- secretKey: POSTGRES_PASSWORD
remoteRef:
key: backstage/database
property: password
- secretKey: GITHUB_TOKEN
remoteRef:
key: backstage/github
property: token
Deployment Manifest
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: backstage
labels:
app: backstage
spec:
replicas: 2
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
serviceAccountName: backstage
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: backstage
image: company/backstage:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 7007
protocol: TCP
env:
- name: NODE_ENV
value: production
- name: LOG_LEVEL
value: info
- name: POSTGRES_HOST
value: backstage-postgres
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_DB
livenessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
volumeMounts:
- name: app-config
mountPath: /app/app-config.production.yaml
subPath: app-config.production.yaml
readOnly: true
volumes:
- name: app-config
configMap:
name: backstage-config
imagePullSecrets:
- name: registry-credentials
Service and Ingress
# kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: backstage
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 7007
protocol: TCP
name: http
selector:
app: backstage
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backstage
namespace: backstage
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
ingressClassName: nginx
tls:
- hosts:
- idp.company.com
secretName: backstage-tls
rules:
- host: idp.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backstage
port:
number: 80
RBAC Configuration
# kubernetes/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: backstage
namespace: backstage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backstage-cluster-reader
rules:
- apiGroups:
- '*'
resources:
- pods
- services
- deployments
- replicasets
- horizontalpodautoscalers
- ingresses
- statefulsets
- daemonsets
- cronjobs
- jobs
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
- applications
- rollouts
- workflows
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backstage-cluster-reader-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backstage-cluster-reader
subjects:
- kind: ServiceAccount
name: backstage
namespace: backstage
High Availability Setup
Multi-Replica Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backstage
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: backstage
Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backstage
namespace: backstage
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backstage
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 2
periodSeconds: 15
selectPolicy: Max
Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: backstage
namespace: backstage
spec:
minAvailable: 1
selector:
matchLabels:
app: backstage
Monitoring and Observability
Prometheus ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: backstage
namespace: backstage
labels:
app: backstage
spec:
selector:
matchLabels:
app: backstage
endpoints:
- port: http
path: /metrics
interval: 30s
Grafana Dashboard
{
"dashboard": {
"title": "Backstage Metrics",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total{job=\"backstage\"}[5m])"
}
]
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(http_requests_total{job=\"backstage\",status=~\"5..\"}[5m])"
}
]
},
{
"title": "Response Time (P95)",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job=\"backstage\"}[5m]))"
}
]
}
]
}
}
Security Hardening
Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backstage
namespace: backstage
spec:
podSelector:
matchLabels:
app: backstage
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 7007
egress:
- to:
- namespaceSelector: { }
podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
name: argo
ports:
- protocol: TCP
port: 443
- ports:
- protocol: TCP
port: 443 # HTTPS
- protocol: TCP
port: 53 # DNS
- protocol: UDP
port: 53 # DNS
Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: backstage
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted