Internal Developer Platforms (IDPs) have become essential for organizations scaling their engineering teams. Backstage, originally developed by Spotify, provides an open-source framework for building developer portals that unify infrastructure tooling, services, and documentation. This guide demonstrates how to implement Backstage at enterprise scale with production-ready configurations.

Building Internal Developer Platforms with Backstage: Enterprise Implementation Guide

Understanding Internal Developer Platforms

An Internal Developer Platform serves as the single pane of glass for developers, providing:

  • Service Catalog: Centralized registry of all services, libraries, and resources
  • Software Templates: Standardized project scaffolding with best practices
  • Documentation Hub: Unified documentation across all teams
  • Infrastructure Abstraction: Self-service access to infrastructure without deep expertise
  • Plugin Ecosystem: Extensible architecture for integrating existing tools

Backstage Architecture Overview

Backstage consists of three core components:

┌─────────────────────────────────────────────────────────┐
│                     Frontend (React)                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │ Catalog  │  │Templates │  │   Docs   │  │ Plugins │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
└─────────────────────────────────────────────────────────┘
                            │
┌─────────────────────────────────────────────────────────┐
│                    Backend (Node.js)                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │ Catalog  │  │Scaffolder│  │TechDocs  │  │ Search  │ │
│  │  Engine  │  │  Engine  │  │  Engine  │  │ Engine  │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
└─────────────────────────────────────────────────────────┘
                            │
┌─────────────────────────────────────────────────────────┐
│                    Data Sources                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │   Git    │  │   K8s    │  │  Cloud   │  │  APIs   │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
└─────────────────────────────────────────────────────────┘

Initial Backstage Setup

Creating the Backstage Application

# Install Backstage CLI
npm install -g @backstage/create-app

# Create new Backstage instance
npx @backstage/create-app --skip-install

cd backstage

# Install dependencies
yarn install

Enterprise Configuration Structure

# app-config.yaml
app:
  title: Enterprise Developer Portal
  baseUrl: https://backstage.company.com

organization:
  name: Company Inc

backend:
  baseUrl: https://backstage.company.com
  listen:
    port: 7007
    host: 0.0.0.0

  csp:
    connect-src: ["'self'", 'http:', 'https:']
    upgrade-insecure-requests: false

  cors:
    origin: https://backstage.company.com
    methods: [GET, POST, PUT, DELETE]
    credentials: true

  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: backstage_catalog
      ssl:
        ca: ${POSTGRES_CA_CERT}

  cache:
    store: redis
    connection: redis://${REDIS_HOST}:${REDIS_PORT}
    useRedisSets: true

auth:
  environment: production
  providers:
    okta:
      production:
        clientId: ${AUTH_OKTA_CLIENT_ID}
        clientSecret: ${AUTH_OKTA_CLIENT_SECRET}
        audience: ${AUTH_OKTA_AUDIENCE}
        authServerId: ${AUTH_OKTA_SERVER_ID}
        idp: ${AUTH_OKTA_IDP}

catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration

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

  locations:
    - type: url
      target: https://github.com/company/backstage-catalog/blob/main/catalog-info.yaml

    # Discover all catalog-info.yaml files in organization
    - type: github-discovery
      target: https://github.com/company/*/blob/main/catalog-info.yaml

  processors:
    githubOrg:
      providers:
        - target: https://github.com
          apiBaseUrl: https://api.github.com
          token: ${GITHUB_TOKEN}

integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}
      apiBaseUrl: https://api.github.com

  gitlab:
    - host: gitlab.company.com
      token: ${GITLAB_TOKEN}
      apiBaseUrl: https://gitlab.company.com/api/v4

  azure:
    - host: dev.azure.com
      token: ${AZURE_TOKEN}

kubernetes:
  serviceLocatorMethod:
    type: 'multiTenant'
  clusterLocatorMethods:
    - type: 'config'
      clusters:
        - url: https://k8s-prod.company.com
          name: production
          authProvider: 'serviceAccount'
          skipTLSVerify: false
          caData: ${K8S_PROD_CA}
          serviceAccountToken: ${K8S_PROD_TOKEN}
          dashboardUrl: https://k8s-dashboard-prod.company.com

        - url: https://k8s-staging.company.com
          name: staging
          authProvider: 'serviceAccount'
          skipTLSVerify: false
          caData: ${K8S_STAGING_CA}
          serviceAccountToken: ${K8S_STAGING_TOKEN}

techdocs:
  builder: 'external'
  generator:
    runIn: 'docker'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: ${TECHDOCS_S3_BUCKET}
      region: ${AWS_REGION}
      accountId: ${AWS_ACCOUNT_ID}

scaffolder:
  defaultAuthor:
    name: Backstage System
    email: backstage@company.com

  defaultCommitMessage: 'Initial commit from Backstage template'

proxy:
  '/prometheus/api':
    target: https://prometheus.company.com/api/v1/
    changeOrigin: true
    secure: true
    headers:
      Authorization: Bearer ${PROMETHEUS_TOKEN}

  '/grafana/api':
    target: https://grafana.company.com/
    changeOrigin: true
    secure: true
    headers:
      Authorization: Bearer ${GRAFANA_TOKEN}

  '/argocd/api':
    target: https://argocd.company.com/api/v1/
    changeOrigin: true
    secure: true
    headers:
      Cookie: ${ARGOCD_AUTH_TOKEN}

search:
  pg:
    highlightOptions:
      useHighlight: true
      maxWords: 35
      minWords: 15
      shortWord: 3
      highlightAll: false
      maxFragments: 0
      fragmentDelimiter: ' ... '

  elasticsearch:
    provider: elastic
    clientOptions:
      node: ${ELASTICSEARCH_URL}
      auth:
        username: ${ELASTICSEARCH_USERNAME}
        password: ${ELASTICSEARCH_PASSWORD}

Service Catalog Implementation

Entity Descriptor Schemas

# catalog-info.yaml - API Definition
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: payment-api
  description: Payment processing API
  annotations:
    github.com/project-slug: company/payment-service
    backstage.io/techdocs-ref: dir:.
    prometheus.io/rule: payment_api_requests_total
    grafana/dashboard-selector: payment-api-dashboard
    pagerduty.com/integration-key: payment-api-key
    sonarqube.org/project-key: payment-api
spec:
  type: openapi
  lifecycle: production
  owner: payments-team
  system: payment-system
  definition:
    $text: https://github.com/company/payment-service/blob/main/openapi.yaml

---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  description: Payment processing microservice
  tags:
    - java
    - spring-boot
    - payments
    - pci-dss
  annotations:
    github.com/project-slug: company/payment-service
    backstage.io/techdocs-ref: dir:.
    jenkins.io/job-full-name: payment-service/main
    sonarqube.org/project-key: payment-service
    snyk.io/org-id: company
    snyk.io/project-ids: payment-service-id
    circleci.com/project-slug: github/company/payment-service
    pagerduty.com/integration-key: payment-service-key
    opsgenie.com/component-selector: payment-service
    datadog/dashboard-url: https://app.datadoghq.com/dashboard/payment-service
    newrelic.com/dashboard-guid: payment-service-guid
  links:
    - url: https://wiki.company.com/payment-service
      title: Wiki
      icon: docs
    - url: https://grafana.company.com/d/payment-service
      title: Grafana Dashboard
      icon: dashboard
    - url: https://payment-service.company.com
      title: Production Service
      icon: web
    - url: https://runbooks.company.com/payment-service
      title: Runbook
      icon: catalog
spec:
  type: service
  lifecycle: production
  owner: payments-team
  system: payment-system
  providesApis:
    - payment-api
  consumesApis:
    - fraud-detection-api
    - notification-api
  dependsOn:
    - resource:payment-database
    - resource:payment-cache
    - component:fraud-detection-service

---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
  name: payment-system
  description: Complete payment processing system
  annotations:
    backstage.io/techdocs-ref: dir:.
spec:
  owner: payments-team
  domain: financial-services

---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
  name: payment-database
  description: PostgreSQL database for payments
  annotations:
    aws.com/arn: arn:aws:rds:us-east-1:123456789012:db:payment-prod
spec:
  type: database
  owner: payments-team
  system: payment-system

---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
  name: payments-team
  description: Payment processing team
spec:
  type: team
  profile:
    displayName: Payments Team
    email: payments@company.com
    picture: https://avatars.company.com/teams/payments
  parent: financial-services
  children: []
  members:
    - user:john.doe
    - user:jane.smith
    - user:bob.johnson

---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
  name: john.doe
  description: Senior Backend Engineer
spec:
  profile:
    displayName: John Doe
    email: john.doe@company.com
    picture: https://avatars.company.com/john.doe
  memberOf:
    - payments-team

Custom Catalog Processor

// packages/backend/src/plugins/catalog/processors/CustomProcessor.ts
import {
  CatalogProcessor,
  CatalogProcessorEmit,
  processingResult,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Entity } from '@backstage/catalog-model';
import { Logger } from 'winston';

export class CustomEnrichmentProcessor implements CatalogProcessor {
  constructor(private readonly logger: Logger) {}

  getProcessorName(): string {
    return 'CustomEnrichmentProcessor';
  }

  async preProcessEntity(
    entity: Entity,
    location: LocationSpec,
    emit: CatalogProcessorEmit,
  ): Promise<Entity> {
    // Add custom annotations based on entity type
    if (entity.kind === 'Component') {
      // Automatically add monitoring links
      if (!entity.metadata.annotations?.['grafana/dashboard-url']) {
        const componentName = entity.metadata.name;
        entity.metadata.annotations = {
          ...entity.metadata.annotations,
          'grafana/dashboard-url': `https://grafana.company.com/d/${componentName}`,
        };
      }

      // Add cost center based on team
      const owner = entity.spec?.owner as string;
      const costCenter = await this.getCostCenterForTeam(owner);
      entity.metadata.annotations = {
        ...entity.metadata.annotations,
        'company.com/cost-center': costCenter,
      };

      // Add compliance tags
      const tags = entity.metadata.tags || [];
      if (tags.includes('pci-dss')) {
        entity.metadata.annotations = {
          ...entity.metadata.annotations,
          'company.com/compliance': 'pci-dss',
          'company.com/data-classification': 'confidential',
        };
      }
    }

    return entity;
  }

  async postProcessEntity(
    entity: Entity,
    location: LocationSpec,
    emit: CatalogProcessorEmit,
  ): Promise<Entity> {
    // Validate required annotations
    if (entity.kind === 'Component' && entity.spec?.lifecycle === 'production') {
      const requiredAnnotations = [
        'github.com/project-slug',
        'pagerduty.com/integration-key',
        'grafana/dashboard-url',
      ];

      const missingAnnotations = requiredAnnotations.filter(
        annotation => !entity.metadata.annotations?.[annotation],
      );

      if (missingAnnotations.length > 0) {
        this.logger.warn(
          `Component ${entity.metadata.name} missing required annotations: ${missingAnnotations.join(', ')}`,
        );
      }
    }

    return entity;
  }

  async validateEntityKind(entity: Entity): Promise<boolean> {
    return true;
  }

  private async getCostCenterForTeam(teamName: string): Promise<string> {
    // Implement logic to fetch cost center from internal API
    const costCenterMap: Record<string, string> = {
      'payments-team': 'CC-1001',
      'platform-team': 'CC-2001',
      'data-team': 'CC-3001',
    };
    return costCenterMap[teamName] || 'CC-9999';
  }
}

Catalog Backend Configuration

// packages/backend/src/plugins/catalog.ts
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { CustomEnrichmentProcessor } from './catalog/processors/CustomProcessor';
import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
import { GitlabEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab';
import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<Router> {
  const builder = await CatalogBuilder.create(env);

  // Add standard processors
  builder.addProcessor(new ScaffolderEntitiesProcessor());
  builder.addProcessor(new CustomEnrichmentProcessor(env.logger));

  // GitHub Entity Provider for automatic discovery
  builder.addEntityProvider(
    GithubEntityProvider.fromConfig(env.config, {
      logger: env.logger,
      schedule: env.scheduler.createScheduledTaskRunner({
        frequency: { hours: 1 },
        timeout: { minutes: 15 },
      }),
    }),
  );

  // GitLab Entity Provider
  builder.addEntityProvider(
    GitlabEntityProvider.fromConfig(env.config, {
      logger: env.logger,
      schedule: env.scheduler.createScheduledTaskRunner({
        frequency: { hours: 1 },
        timeout: { minutes: 15 },
      }),
    }),
  );

  // LDAP Provider for user/group sync
  builder.addEntityProvider(
    LdapOrgEntityProvider.fromConfig(env.config, {
      id: 'production',
      target: 'ldaps://ldap.company.com',
      logger: env.logger,
      schedule: env.scheduler.createScheduledTaskRunner({
        frequency: { hours: 4 },
        timeout: { minutes: 30 },
      }),
    }),
  );

  const { processingEngine, router } = await builder.build();
  await processingEngine.start();

  return router;
}

Software Templates (Scaffolder)

Comprehensive Template Example

# templates/spring-boot-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: spring-boot-service
  title: Spring Boot Microservice
  description: Create a new Spring Boot microservice with best practices
  tags:
    - java
    - spring-boot
    - microservice
    - recommended
spec:
  owner: platform-team
  type: service

  parameters:
    - title: Service Information
      required:
        - name
        - description
        - owner
      properties:
        name:
          title: Name
          type: string
          description: Unique name of the service
          pattern: '^[a-z0-9-]+$'
          ui:autofocus: true
          ui:help: 'Must be lowercase with hyphens'

        description:
          title: Description
          type: string
          description: Brief description of the service
          ui:widget: textarea
          ui:options:
            rows: 3

        owner:
          title: Owner
          type: string
          description: Team that owns this service
          ui:field: OwnerPicker
          ui:options:
            allowedKinds:
              - Group

        system:
          title: System
          type: string
          description: System this service belongs to
          ui:field: EntityPicker
          ui:options:
            allowedKinds:
              - System
            defaultKind: System

    - title: Technical Configuration
      required:
        - javaVersion
        - springBootVersion
      properties:
        javaVersion:
          title: Java Version
          type: string
          enum:
            - '17'
            - '21'
          default: '21'

        springBootVersion:
          title: Spring Boot Version
          type: string
          enum:
            - '3.2.0'
            - '3.1.5'
          default: '3.2.0'

        database:
          title: Database
          type: string
          enum:
            - none
            - postgresql
            - mysql
            - mongodb
          default: postgresql
          enumNames:
            - 'No Database'
            - 'PostgreSQL'
            - 'MySQL'
            - 'MongoDB'

        cache:
          title: Caching
          type: boolean
          default: true
          description: Enable Redis caching

        messaging:
          title: Messaging
          type: string
          enum:
            - none
            - kafka
            - rabbitmq
          default: kafka
          enumNames:
            - 'No Messaging'
            - 'Apache Kafka'
            - 'RabbitMQ'

        observability:
          title: Observability Stack
          type: array
          items:
            type: string
            enum:
              - prometheus
              - grafana
              - jaeger
              - loki
          uniqueItems: true
          default:
            - prometheus
            - grafana
            - jaeger

    - title: Repository Configuration
      required:
        - repoUrl
      properties:
        repoUrl:
          title: Repository Location
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com
              - gitlab.company.com
            allowedOwners:
              - company

        visibility:
          title: Repository Visibility
          type: string
          enum:
            - public
            - internal
            - private
          default: internal

    - title: Deployment Configuration
      properties:
        namespace:
          title: Kubernetes Namespace
          type: string
          default: default
          pattern: '^[a-z0-9-]+$'

        replicas:
          title: Initial Replicas
          type: number
          default: 3
          minimum: 1
          maximum: 10

        enableHPA:
          title: Enable Horizontal Pod Autoscaling
          type: boolean
          default: true

        enableIstio:
          title: Enable Istio Service Mesh
          type: boolean
          default: true

        environments:
          title: Deployment Environments
          type: array
          items:
            type: string
            enum:
              - dev
              - staging
              - production
          uniqueItems: true
          default:
            - dev
            - staging
            - production

  steps:
    - id: fetch-base
      name: Fetch Base Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          javaVersion: ${{ parameters.javaVersion }}
          springBootVersion: ${{ parameters.springBootVersion }}
          database: ${{ parameters.database }}
          cache: ${{ parameters.cache }}
          messaging: ${{ parameters.messaging }}
          observability: ${{ parameters.observability }}
          namespace: ${{ parameters.namespace }}
          replicas: ${{ parameters.replicas }}
          enableHPA: ${{ parameters.enableHPA }}
          enableIstio: ${{ parameters.enableIstio }}

    - id: create-repo
      name: Create Repository
      action: publish:github
      input:
        allowedHosts:
          - github.com
        description: ${{ parameters.description }}
        repoUrl: ${{ parameters.repoUrl }}
        repoVisibility: ${{ parameters.visibility }}
        defaultBranch: main
        protectDefaultBranch: true
        requiredStatusChecks:
          - build
          - test
          - security-scan
        requireCodeOwnerReviews: true
        dismissStaleReviews: true
        requiredApprovingReviewCount: 2

    - id: create-pull-request
      name: Create Initial PR
      action: publish:github:pull-request
      input:
        repoUrl: ${{ parameters.repoUrl }}
        branchName: scaffolder-initial-commit
        title: 'Initial commit from Backstage template'
        description: |
          This PR contains the initial scaffolded code for ${{ parameters.name }}.

          Generated with the following configuration:
          - Java Version: ${{ parameters.javaVersion }}
          - Spring Boot Version: ${{ parameters.springBootVersion }}
          - Database: ${{ parameters.database }}
          - Cache: ${{ parameters.cache }}
          - Messaging: ${{ parameters.messaging }}

          Please review and merge to complete the setup.

    - id: register-catalog
      name: Register in Catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.create-repo.output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'

    - id: create-kubernetes-namespace
      name: Create Kubernetes Namespace
      action: kubernetes:create-namespace
      input:
        namespace: ${{ parameters.namespace }}
        labels:
          app.kubernetes.io/managed-by: backstage
          app.kubernetes.io/name: ${{ parameters.name }}
          team: ${{ parameters.owner }}

    - id: setup-argocd-application
      name: Setup ArgoCD Application
      action: argocd:create-application
      input:
        name: ${{ parameters.name }}
        namespace: argocd
        project: default
        source:
          repoURL: ${{ steps.create-repo.output.remoteUrl }}
          path: kubernetes
          targetRevision: main
        destination:
          server: https://kubernetes.default.svc
          namespace: ${{ parameters.namespace }}
        syncPolicy:
          automated:
            prune: true
            selfHeal: true
          syncOptions:
            - CreateNamespace=true

    - id: create-grafana-dashboard
      name: Create Grafana Dashboard
      action: grafana:create-dashboard
      input:
        title: ${{ parameters.name }} Service Dashboard
        tags:
          - backstage
          - ${{ parameters.name }}
        folder: Services

    - id: setup-pagerduty
      name: Setup PagerDuty Integration
      action: pagerduty:create-service
      input:
        name: ${{ parameters.name }}
        description: ${{ parameters.description }}
        escalationPolicyId: ${{ parameters.owner }}-policy
        alertCreation: create_alerts_and_incidents

    - id: create-jira-project
      name: Create Jira Project
      action: jira:create-project
      input:
        key: ${{ parameters.name | upper | replace('-', '') }}
        name: ${{ parameters.name }}
        projectTypeKey: software
        projectTemplateKey: com.pyxis.greenhopper.jira:gh-simplified-agility-kanban
        lead: ${{ parameters.owner }}

  output:
    links:
      - title: Repository
        url: ${{ steps.create-repo.output.remoteUrl }}
      - title: Pull Request
        url: ${{ steps.create-pull-request.output.pullRequestUrl }}
      - title: Catalog Entry
        icon: catalog
        entityRef: ${{ steps.register-catalog.output.entityRef }}
      - title: ArgoCD Application
        url: https://argocd.company.com/applications/${{ parameters.name }}
      - title: Grafana Dashboard
        url: ${{ steps.create-grafana-dashboard.output.dashboardUrl }}
      - title: PagerDuty Service
        url: ${{ steps.setup-pagerduty.output.serviceUrl }}
      - title: Jira Project
        url: ${{ steps.create-jira-project.output.projectUrl }}

Custom Scaffolder Actions

// packages/backend/src/plugins/scaffolder/actions/kubernetes.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { KubeConfig, CoreV1Api } from '@kubernetes/client-node';

export const createKubernetesNamespaceAction = () => {
  return createTemplateAction<{
    namespace: string;
    labels?: Record<string, string>;
    annotations?: Record<string, string>;
  }>({
    id: 'kubernetes:create-namespace',
    description: 'Creates a Kubernetes namespace',
    schema: {
      input: {
        required: ['namespace'],
        type: 'object',
        properties: {
          namespace: {
            type: 'string',
            title: 'Namespace',
            description: 'Name of the namespace to create',
          },
          labels: {
            type: 'object',
            title: 'Labels',
            description: 'Labels to apply to the namespace',
          },
          annotations: {
            type: 'object',
            title: 'Annotations',
            description: 'Annotations to apply to the namespace',
          },
        },
      },
      output: {
        type: 'object',
        properties: {
          namespace: {
            type: 'string',
            title: 'Created namespace name',
          },
        },
      },
    },
    async handler(ctx) {
      const { namespace, labels, annotations } = ctx.input;

      const kc = new KubeConfig();
      kc.loadFromDefault();
      const k8sApi = kc.makeApiClient(CoreV1Api);

      try {
        // Check if namespace already exists
        try {
          await k8sApi.readNamespace(namespace);
          ctx.logger.info(`Namespace ${namespace} already exists`);
          ctx.output('namespace', namespace);
          return;
        } catch (e: any) {
          if (e.statusCode !== 404) {
            throw e;
          }
        }

        // Create namespace
        await k8sApi.createNamespace({
          metadata: {
            name: namespace,
            labels: {
              'app.kubernetes.io/managed-by': 'backstage',
              ...labels,
            },
            annotations: {
              'backstage.io/created-at': new Date().toISOString(),
              ...annotations,
            },
          },
        });

        ctx.logger.info(`Created namespace ${namespace}`);
        ctx.output('namespace', namespace);

        // Create default network policies
        await createDefaultNetworkPolicies(k8sApi, namespace);

        // Create resource quotas
        await createResourceQuotas(k8sApi, namespace);

        // Create limit ranges
        await createLimitRanges(k8sApi, namespace);

      } catch (error: any) {
        ctx.logger.error(`Failed to create namespace: ${error.message}`);
        throw new Error(`Failed to create namespace: ${error.message}`);
      }
    },
  });
};

async function createDefaultNetworkPolicies(
  k8sApi: CoreV1Api,
  namespace: string,
): Promise<void> {
  // Implementation for creating default network policies
}

async function createResourceQuotas(
  k8sApi: CoreV1Api,
  namespace: string,
): Promise<void> {
  // Implementation for creating resource quotas
}

async function createLimitRanges(
  k8sApi: CoreV1Api,
  namespace: string,
): Promise<void> {
  // Implementation for creating limit ranges
}

Plugin Development

Custom Plugin: Cost Insights

// plugins/cost-insights/src/plugin.ts
import {
  createPlugin,
  createRoutableExtension,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';

export const costInsightsPlugin = createPlugin({
  id: 'cost-insights',
  routes: {
    root: rootRouteRef,
  },
});

export const CostInsightsPage = costInsightsPlugin.provide(
  createRoutableExtension({
    name: 'CostInsightsPage',
    component: () =>
      import('./components/CostInsightsPage').then(m => m.CostInsightsPage),
    mountPoint: rootRouteRef,
  }),
);

// plugins/cost-insights/src/components/CostInsightsPage.tsx
import React from 'react';
import { InfoCard, Header, Page, Content } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { Grid, Typography } from '@material-ui/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
  BarChart,
  Bar,
} from 'recharts';

export const CostInsightsPage = () => {
  const { entity } = useEntity();
  const config = useApi(configApiRef);
  const [costData, setCostData] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetchCostData();
  }, [entity]);

  const fetchCostData = async () => {
    try {
      const response = await fetch(
        `${config.getString('backend.baseUrl')}/api/cost-insights/${entity.metadata.name}`,
      );
      const data = await response.json();
      setCostData(data);
    } catch (error) {
      console.error('Failed to fetch cost data:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Page themeId="tool">
      <Header title="Cost Insights" subtitle={entity.metadata.name} />
      <Content>
        <Grid container spacing={3}>
          <Grid item xs={12} md={6}>
            <InfoCard title="Monthly Cost Trend">
              <ResponsiveContainer width="100%" height={300}>
                <LineChart data={costData}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="month" />
                  <YAxis />
                  <Tooltip />
                  <Legend />
                  <Line type="monotone" dataKey="compute" stroke="#8884d8" />
                  <Line type="monotone" dataKey="storage" stroke="#82ca9d" />
                  <Line type="monotone" dataKey="network" stroke="#ffc658" />
                </LineChart>
              </ResponsiveContainer>
            </InfoCard>
          </Grid>

          <Grid item xs={12} md={6}>
            <InfoCard title="Cost by Resource Type">
              <ResponsiveContainer width="100%" height={300}>
                <BarChart data={costData}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="resourceType" />
                  <YAxis />
                  <Tooltip />
                  <Legend />
                  <Bar dataKey="cost" fill="#8884d8" />
                </BarChart>
              </ResponsiveContainer>
            </InfoCard>
          </Grid>

          <Grid item xs={12}>
            <InfoCard title="Cost Optimization Recommendations">
              <Typography variant="body1">
                {/* Recommendations based on cost analysis */}
              </Typography>
            </InfoCard>
          </Grid>
        </Grid>
      </Content>
    </Page>
  );
};

Production Deployment

Docker Configuration

# Dockerfile
FROM node:18-bullseye-slim AS packages

WORKDIR /app
COPY package.json yarn.lock ./
COPY packages packages
RUN yarn install --frozen-lockfile --production --network-timeout 600000

FROM node:18-bullseye-slim AS build

WORKDIR /app
COPY --from=packages /app/node_modules ./node_modules
COPY . .

RUN yarn tsc
RUN yarn build:backend --config ../../app-config.yaml

FROM node:18-bullseye-slim

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python3 \
    g++ \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

RUN useradd -r -u 1001 -g root backstage
WORKDIR /app

COPY --from=build --chown=backstage:root /app/yarn.lock /app/package.json ./
COPY --from=build --chown=backstage:root /app/node_modules ./node_modules
COPY --from=build --chown=backstage:root /app/packages ./packages
COPY --from=build --chown=backstage:root /app/plugins ./plugins

ENV NODE_ENV production

USER backstage
EXPOSE 7007

CMD ["node", "packages/backend", "--config", "app-config.yaml"]

Kubernetes Deployment

# kubernetes/backstage-deployment.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: backstage
  labels:
    name: backstage
    app.kubernetes.io/managed-by: kubectl

---
apiVersion: v1
kind: Secret
metadata:
  name: backstage-secrets
  namespace: backstage
type: Opaque
stringData:
  POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
  GITHUB_TOKEN: "${GITHUB_TOKEN}"
  AUTH_OKTA_CLIENT_SECRET: "${AUTH_OKTA_CLIENT_SECRET}"

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: backstage-config
  namespace: backstage
data:
  app-config.production.yaml: |
    app:
      baseUrl: https://backstage.company.com
    backend:
      baseUrl: https://backstage.company.com
      database:
        client: pg
        connection:
          host: postgres-service
          port: 5432
          user: backstage
          database: backstage_catalog

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: backstage
  labels:
    app: backstage
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backstage
  template:
    metadata:
      labels:
        app: backstage
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "7007"
        prometheus.io/path: "/metrics"
    spec:
      serviceAccountName: backstage
      containers:
        - name: backstage
          image: company/backstage:latest
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 7007
          envFrom:
            - secretRef:
                name: backstage-secrets
          volumeMounts:
            - name: config
              mountPath: /app/app-config.production.yaml
              subPath: app-config.production.yaml
          livenessProbe:
            httpGet:
              path: /healthcheck
              port: 7007
            initialDelaySeconds: 60
            periodSeconds: 10
            timeoutSeconds: 5
          readinessProbe:
            httpGet:
              path: /healthcheck
              port: 7007
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
      volumes:
        - name: config
          configMap:
            name: backstage-config

---
apiVersion: v1
kind: Service
metadata:
  name: backstage
  namespace: backstage
spec:
  selector:
    app: backstage
  ports:
    - name: http
      port: 80
      targetPort: 7007
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backstage
  namespace: backstage
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - backstage.company.com
      secretName: backstage-tls
  rules:
    - host: backstage.company.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: backstage
                port:
                  number: 80

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: backstage
  namespace: backstage
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: backstage
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: backstage
  namespace: backstage
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: backstage

Monitoring and Observability

Prometheus Metrics

// packages/backend/src/plugins/metrics.ts
import { Request, Response, NextFunction } from 'express';
import promClient from 'prom-client';

const httpRequestDuration = new promClient.Histogram({
  name: 'backstage_http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5],
});

const catalogEntitiesTotal = new promClient.Gauge({
  name: 'backstage_catalog_entities_total',
  help: 'Total number of entities in catalog',
  labelNames: ['kind', 'type'],
});

const scaffolderTasksTotal = new promClient.Counter({
  name: 'backstage_scaffolder_tasks_total',
  help: 'Total number of scaffolder tasks',
  labelNames: ['template', 'status'],
});

export const metricsMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode.toString())
      .observe(duration);
  });

  next();
};

export const metricsEndpoint = async (req: Request, res: Response) => {
  res.set('Content-Type', promClient.register.contentType);
  res.end(await promClient.register.metrics());
};

Conclusion

Implementing Backstage as an Internal Developer Platform provides organizations with a unified interface for all development activities. Key benefits include:

  • Developer Productivity: Self-service capabilities reduce wait times
  • Standardization: Templates enforce best practices across teams
  • Discoverability: Centralized catalog makes finding resources easy
  • Integration: Single pane of glass for all development tools
  • Scalability: Plugin architecture supports growing tool ecosystems

Success requires commitment to maintaining the catalog, developing useful templates, and continuous improvement based on developer feedback.