Initial Backstage Helm chart with ESO integration

This commit is contained in:
Backstage Deploy 2026-01-14 15:09:56 +05:30
commit eb39a1cd85
13 changed files with 709 additions and 0 deletions

12
Chart.yaml Normal file
View File

@ -0,0 +1,12 @@
apiVersion: v2
name: backstage
description: Backstage Developer Portal for ephemeral testing environments
type: application
version: 1.0.0
appVersion: "1.28.4"
keywords:
- backstage
- developer-portal
- platform-engineering
maintainers:
- name: OnlineSales Platform Team

60
templates/_helpers.tpl Normal file
View File

@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "backstage.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "backstage.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "backstage.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "backstage.labels" -}}
helm.sh/chart: {{ include "backstage.chart" . }}
{{ include "backstage.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "backstage.selectorLabels" -}}
app.kubernetes.io/name: {{ include "backstage.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Service account name
*/}}
{{- define "backstage.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "backstage.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,123 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: backstage-config
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
data:
app-config.production.yaml: |
app:
title: {{ .Values.backstage.title }}
baseUrl: {{ .Values.backstage.baseUrl }}
organization:
name: {{ .Values.backstage.organization }}
backend:
baseUrl: {{ .Values.backstage.baseUrl }}
listen:
port: {{ .Values.service.targetPort }}
host: 0.0.0.0
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: {{ .Values.backstage.baseUrl }}
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: {{ .Values.database.name }}
ssl:
rejectUnauthorized: false
auth:
keys:
- secret: ${BACKEND_SECRET}
auth:
environment: production
session:
secret: ${BACKEND_SECRET}
providers:
google:
production:
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
integrations:
gitea:
- baseUrl: {{ .Values.integrations.gitea.url }}
host: {{ .Values.integrations.gitea.url | replace "https://" "" | replace "http://" "" }}
username: ${GITEA_USERNAME}
password: ${GITEA_PASSWORD}
proxy:
endpoints:
'/argo-workflows/api':
target: '{{ .Values.integrations.argoWorkflows.url }}'
changeOrigin: true
secure: true
headers:
Accept: 'application/json'
Authorization: 'Bearer ${ARGO_WORKFLOWS_TOKEN}'
'/argocd/api':
target: '{{ .Values.integrations.argocd.url }}'
changeOrigin: true
secure: true
techdocs:
builder: 'local'
generator:
runIn: 'local'
publisher:
type: 'local'
scaffolder:
defaultAuthor:
name: backstage-scaffolder
email: noreply@{{ .Values.backstage.baseUrl | replace "https://" "" | replace "http://" "" }}
defaultCommitMessage: "backstage scaffolder"
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location, Template]
locations:
- type: url
target: {{ .Values.integrations.gitea.url }}/templates/catalog-info.yaml
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: {{ .Values.integrations.kubernetes.clusterUrl }}
name: backstage-cluster
authProvider: 'serviceAccount'
skipTLSVerify: false
skipMetricsLookup: false
argocd:
appLocatorMethods:
- type: 'config'
instances:
- name: production
url: {{ .Values.integrations.argocd.url }}
username: admin
password: ${ARGOCD_AUTH_TOKEN}
argoWorkflows:
baseUrl: {{ .Values.integrations.argoWorkflows.url }}

View File

@ -0,0 +1,129 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "backstage.fullname" . }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "backstage.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
{{- include "backstage.selectorLabels" . | nindent 8 }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "{{ .Values.service.targetPort }}"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: {{ include "backstage.serviceAccountName" . }}
automountServiceAccountToken: true
securityContext:
fsGroup: 1000
runAsUser: 1000
runAsNonRoot: true
containers:
- name: backstage
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
env:
- name: NODE_ENV
value: "production"
# Database credentials from secret
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_PORT
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: backstage-secrets
key: POSTGRES_PASSWORD
# OAuth credentials
- name: AUTH_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_GOOGLE_CLIENT_ID
- name: AUTH_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_GOOGLE_CLIENT_SECRET
# Backend secret
- name: BACKEND_SECRET
valueFrom:
secretKeyRef:
name: backstage-secrets
key: BACKEND_SECRET
# ArgoCD integration
- name: ARGOCD_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: backstage-secrets
key: ARGOCD_AUTH_TOKEN
# Argo Workflows integration
- name: ARGO_WORKFLOWS_TOKEN
valueFrom:
secretKeyRef:
name: backstage-secrets
key: ARGO_WORKFLOWS_TOKEN
# Gitea integration
- name: GITEA_USERNAME
valueFrom:
secretKeyRef:
name: backstage-secrets
key: GITEA_USERNAME
- name: GITEA_PASSWORD
valueFrom:
secretKeyRef:
name: backstage-secrets
key: GITEA_PASSWORD
volumeMounts:
- name: config
mountPath: /app/app-config.production.yaml
subPath: app-config.production.yaml
readOnly: true
command:
- node
- packages/backend
- --config
- app-config.production.yaml
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
volumes:
- name: config
configMap:
name: backstage-config
restartPolicy: Always

View File

@ -0,0 +1,40 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "backstage.fullname" . }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "backstage.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,51 @@
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backstage-environment-manager
labels:
{{- include "backstage.labels" . | nindent 4 }}
rules:
# Namespace management for environment lifecycle
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch", "delete"]
# Read access to environment resources for display
- apiGroups: [""]
resources: ["resourcequotas", "limitranges", "pods", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["get", "list", "watch"]
# ArgoCD Applications - for cleanup when deleting environments
- apiGroups: ["argoproj.io"]
resources: ["applications"]
verbs: ["get", "list", "watch", "delete"]
# Argo Workflows - for monitoring environment provisioning status
- apiGroups: ["argoproj.io"]
resources: ["workflows"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backstage-environment-manager-binding
labels:
{{- include "backstage.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backstage-environment-manager
subjects:
- kind: ServiceAccount
name: {{ include "backstage.serviceAccountName" . }}
namespace: {{ .Values.namespaceOverride }}
{{- end }}

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "backstage.fullname" . }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.port }}
protocol: TCP
targetPort: {{ .Values.service.targetPort }}
selector:
{{- include "backstage.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,48 @@
{{- if .Values.cloudSqlProxy.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage-cloudsql-proxy
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
app: backstage-cloudsql-proxy
spec:
replicas: 1
selector:
matchLabels:
app: backstage-cloudsql-proxy
template:
metadata:
labels:
app: backstage-cloudsql-proxy
{{- include "backstage.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ .Values.cloudSqlProxyServiceAccount.name }}
automountServiceAccountToken: true
securityContext:
fsGroup: 65532
runAsUser: 65532
runAsNonRoot: true
containers:
- name: cloud-sql-proxy
image: "{{ .Values.cloudSqlProxy.image.repository }}:{{ .Values.cloudSqlProxy.image.tag }}"
args:
- "{{ .Values.cloudSqlProxy.connectionName }}"
- "--port={{ .Values.cloudSqlProxy.port }}"
- "--address=0.0.0.0"
{{- if .Values.cloudSqlProxy.privateIp }}
- "--private-ip"
{{- end }}
ports:
- name: postgres
containerPort: {{ .Values.cloudSqlProxy.port }}
protocol: TCP
securityContext:
runAsNonRoot: true
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
resources:
{{- toYaml .Values.cloudSqlProxy.resources | nindent 10 }}
{{- end }}

View File

@ -0,0 +1,19 @@
{{- if .Values.cloudSqlProxy.enabled -}}
apiVersion: v1
kind: Service
metadata:
name: backstage-cloudsql-proxy
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
app: backstage-cloudsql-proxy
spec:
type: ClusterIP
ports:
- name: postgresql
port: {{ .Values.cloudSqlProxy.port }}
protocol: TCP
targetPort: {{ .Values.cloudSqlProxy.port }}
selector:
app: backstage-cloudsql-proxy
{{- end }}

View File

@ -0,0 +1,20 @@
{{- if .Values.externalSecrets.enabled -}}
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: backstage-secrets
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval }}
secretStoreRef:
name: {{ .Values.externalSecrets.secretStoreRef }}
kind: SecretStore
target:
name: backstage-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: {{ .Values.externalSecrets.secretName }}
{{- end }}

View File

@ -0,0 +1,19 @@
{{- if .Values.externalSecrets.enabled -}}
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: {{ .Values.externalSecrets.secretStoreRef }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
spec:
provider:
gcpsm:
projectID: {{ .Values.externalSecrets.projectId | quote }}
auth:
workloadIdentity:
clusterLocation: {{ .Values.externalSecrets.clusterLocation }}
clusterName: {{ .Values.externalSecrets.clusterName }}
serviceAccountRef:
name: {{ .Values.serviceAccount.name }}
{{- end }}

View File

@ -0,0 +1,30 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}
{{- if .Values.cloudSqlProxyServiceAccount.create }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.cloudSqlProxyServiceAccount.name }}
namespace: {{ .Values.namespaceOverride }}
labels:
{{- include "backstage.labels" . | nindent 4 }}
app: backstage-cloudsql-proxy
{{- with .Values.cloudSqlProxyServiceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}

142
values.yaml Normal file
View File

@ -0,0 +1,142 @@
# Global settings
nameOverride: ""
fullnameOverride: "backstage"
namespaceOverride: "backstage"
# Backstage application image
image:
repository: gcr.io/prj-onlinesales-test-framework/backstage
tag: "v1.0.0"
pullPolicy: IfNotPresent
# Replica count
replicaCount: 2
# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 7007
# Ingress configuration
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
hosts:
- host: backstage.os-tf-qa.onlinesales.ai
paths:
- path: /
pathType: Prefix
tls:
- secretName: backstage-tls
hosts:
- backstage.os-tf-qa.onlinesales.ai
# Resources
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
# Health checks
livenessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Backstage configuration
backstage:
baseUrl: https://backstage.os-tf-qa.onlinesales.ai
title: "OnlineSales Test Framework"
organization: "OnlineSales"
# Database configuration (Cloud SQL via proxy)
database:
host: backstage-cloudsql-proxy
port: 5432
user: backstage-user
name: backstage
# Cloud SQL Proxy configuration
cloudSqlProxy:
enabled: true
image:
repository: gcr.io/cloud-sql-connectors/cloud-sql-proxy
tag: "2.14.0"
connectionName: "prj-onlinesales-test-framework:asia-south1:backstage-db"
port: 5432
privateIp: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
# External Secrets Operator configuration
externalSecrets:
enabled: true
projectId: "prj-onlinesales-test-framework"
clusterLocation: "asia-south1"
clusterName: "backstage-cluster"
refreshInterval: "1h"
secretStoreRef: "gcpsm-secret-store"
# Single secret with all key-value pairs
secretName: "backstage-secrets"
# Integrations
integrations:
argocd:
url: https://argocd.os-tf-qa.onlinesales.ai
argoWorkflows:
url: https://argo-workflows.os-tf-qa.onlinesales.ai
gitea:
url: https://gitea.os-tf-qa.onlinesales.ai
kubernetes:
clusterUrl: https://35.200.246.59
# RBAC
rbac:
create: true
# Service accounts
serviceAccount:
create: true
name: backstage
annotations:
iam.gke.io/gcp-service-account: backstage-workload@prj-onlinesales-test-framework.iam.gserviceaccount.com
cloudSqlProxyServiceAccount:
create: true
name: backstage-cloudsql
annotations:
iam.gke.io/gcp-service-account: backstage-workload@prj-onlinesales-test-framework.iam.gserviceaccount.com
# Namespace
namespace:
create: false # Already created by Terraform
name: backstage