Platform Engineering in 2026: Building Internal Developer Platforms with Backstage



Platform Engineering in 2026: Building Internal Developer Platforms with Backstage

The DevOps movement told every developer to own their infrastructure. The result, a decade later: developers drowning in Kubernetes YAML, Terraform modules, security policies, observability configs, and deployment pipelines. The cognitive load became unsustainable.

Platform Engineering is the response. Instead of “you build it, you run it” meaning “you configure everything yourself,” platform engineering creates golden paths — opinionated, self-service workflows that let developers deploy with confidence without needing to become infrastructure experts.

At the center of most modern IDPs sits Backstage — Spotify’s open-source developer portal, now a CNCF project used by Airbnb, American Airlines, Spotify, and hundreds more.


What Is an Internal Developer Platform?

An IDP is not a CI/CD tool. It’s not a Kubernetes dashboard. It’s an abstraction layer over your entire engineering platform that surfaces the right capabilities at the right level of abstraction for your developers.

A mature IDP provides:

  • Software catalog — Every service, library, API, team, and resource in one place
  • Self-service scaffolding — Create a new production-ready service in minutes
  • Environment management — Spin up dev/staging environments without ops tickets
  • Deployment pipelines — One-click deploys with built-in best practices
  • Observability — Dashboards, traces, and alerts pre-wired for every service
  • Documentation — Auto-generated and always up to date

The North Star: a developer should be able to go from idea to production without filing a ticket or bothering the platform team.


Backstage: The Building Blocks

Software Catalog

The catalog is Backstage’s foundation. It aggregates metadata from all your systems into a unified graph:

# catalog-info.yaml (lives in every service repo)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payments-service
  description: Handles all payment processing and billing
  annotations:
    github.com/project-slug: myorg/payments-service
    grafana/dashboard-selector: "service=payments"
    pagerduty.com/service-id: P1234567
    sonarqube.org/project-key: payments-service
    backstage.io/techdocs-ref: dir:.
  tags:
    - golang
    - payments
    - critical
  links:
    - url: https://grafana.internal/d/payments
      title: Production Dashboard
    - url: https://payments.internal/docs/api
      title: API Documentation
spec:
  type: service
  lifecycle: production
  owner: team:payments
  system: billing-platform
  dependsOn:
    - component:postgres-payments
    - component:stripe-integration
    - resource:payments-kafka-topic
  providesApis:
    - payments-api

Backstage discovers these files via GitHub, GitLab, or Bitbucket integrations and builds a live dependency graph of your entire organization’s software.


Software Templates: The Golden Path

Templates are where platform engineering delivers its biggest ROI. A well-designed template gives a developer a production-ready service with:

  • Proper project structure
  • Dockerized build
  • CI/CD pipeline
  • Kubernetes manifests
  • Monitoring dashboards
  • Alert rules
  • Auto-registered in the catalog

All from a single form fill.

# templates/golang-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golang-microservice
  title: Go Microservice
  description: Production-ready Go service with Kubernetes, monitoring, and CI/CD
  tags:
    - golang
    - recommended
spec:
  owner: platform-team
  type: service
  
  parameters:
    - title: Service Details
      required: [name, description, owner]
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z][a-z0-9-]*[a-z0-9]$'
          description: Lowercase, hyphenated (e.g., user-service)
        description:
          title: Description
          type: string
        owner:
          title: Owner Team
          type: string
          ui:field: OwnerPicker
          ui:options:
            allowedKinds: [Group]
        
    - title: Infrastructure
      properties:
        database:
          title: Database
          type: string
          enum: [none, postgresql, mysql, mongodb]
          default: none
        cache:
          title: Cache
          type: string
          enum: [none, redis]
          default: none
        initialReplicas:
          title: Initial Replica Count
          type: integer
          default: 2
          minimum: 1
          maximum: 10
  
  steps:
    - id: fetch-template
      name: Fetch Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: $
          description: $
          owner: $
          database: $
          cache: $
    
    - id: create-repo
      name: Create GitHub Repository
      action: github:repo:create
      input:
        repoUrl: github.com?owner=myorg&repo=$
        description: $
        defaultBranch: main
        topics:
          - go
          - microservice
    
    - id: push-code
      name: Push Initial Code
      action: github:repo:push
      input:
        repoUrl: github.com?owner=myorg&repo=$
        defaultBranch: main
    
    - id: create-pagerduty-service
      name: Create PagerDuty Service
      action: pagerduty:service:create
      input:
        name: $
        escalationPolicyId: P_PLATFORM_DEFAULT
    
    - id: register-catalog
      name: Register in Catalog
      action: catalog:register
      input:
        repoContentsUrl: $
        catalogInfoPath: /catalog-info.yaml
  
  output:
    links:
      - title: Repository
        url: $
      - title: Open in Catalog
        entityRef: $

Backstage developer portal showing service catalog and templates Photo by Marvin Meyer on Unsplash


Custom Plugins: Extending Backstage

Backstage’s plugin system is where it gets powerful. Every company’s platform is different — custom plugins let you surface the exact right information for your developers.

Building a Custom Plugin

// plugins/my-deployments/src/components/DeploymentList.tsx
import React, { useEffect, useState } from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useApi } from '@backstage/core-plugin-api';
import { myDeploymentsApiRef } from '../api';

export const DeploymentList = () => {
  const { entity } = useEntity();
  const deploymentsApi = useApi(myDeploymentsApiRef);
  const [deployments, setDeployments] = useState([]);
  
  useEffect(() => {
    const serviceName = entity.metadata.name;
    deploymentsApi.getDeployments(serviceName).then(setDeployments);
  }, [entity.metadata.name, deploymentsApi]);
  
  return (
    <InfoCard title="Recent Deployments">
      <Table
        columns={[
          { title: 'Version', field: 'version' },
          { title: 'Environment', field: 'environment' },
          { title: 'Deployed By', field: 'deployedBy' },
          { title: 'Status', field: 'status', render: row => (
            <StatusBadge status={row.status} />
          )},
          { title: 'Time', field: 'deployedAt', render: row => (
            <RelativeTime timestamp={row.deployedAt} />
          )},
        ]}
        data={deployments}
        options=
      />
    </InfoCard>
  );
};
// Register the plugin in your Backstage app
// packages/app/src/components/catalog/EntityPage.tsx
import { DeploymentList } from '@mycompany/backstage-plugin-deployments';

const serviceEntityPage = (
  <EntityLayout>
    <EntityLayout.Route path="/" title="Overview">
      <EntityAboutCard />
    </EntityLayout.Route>
    
    <EntityLayout.Route path="/deployments" title="Deployments">
      <DeploymentList />
    </EntityLayout.Route>
    
    <EntityLayout.Route path="/kubernetes" title="Kubernetes">
      <EntityKubernetesContent />
    </EntityLayout.Route>
    
    <EntityLayout.Route path="/ci-cd" title="CI/CD">
      <EntityGithubActionsContent />
    </EntityLayout.Route>
    
    <EntityLayout.Route path="/monitoring" title="Monitoring">
      <EntityGrafanaDashboardsCard />
    </EntityLayout.Route>
  </EntityLayout>
);

Environment Management

One of the highest-value IDP capabilities: self-service ephemeral environments.

// Backstage action for creating preview environments
createTemplateAction({
  id: 'mycompany:environment:create',
  schema: {
    input: z.object({
      serviceName: z.string(),
      prNumber: z.number(),
      ttlHours: z.number().default(48),
    }),
  },
  async handler(ctx) {
    const { serviceName, prNumber, ttlHours } = ctx.input;
    const envName = `pr-${prNumber}-${serviceName}`;
    
    // Create namespace in Kubernetes
    await k8sApi.createNamespace({
      metadata: {
        name: envName,
        labels: {
          'environment-type': 'ephemeral',
          'pr-number': String(prNumber),
          'service': serviceName,
          'ttl-hours': String(ttlHours),
        },
      },
    });
    
    // Deploy service with Helm
    await helmClient.upgrade(envName, `./charts/${serviceName}`, {
      namespace: envName,
      values: {
        image: { tag: `pr-${prNumber}` },
        ingress: { host: `${envName}.preview.example.com` },
        replicas: 1,  // reduced for preview
      },
    });
    
    ctx.output('previewUrl', `https://${envName}.preview.example.com`);
    ctx.output('namespaceName', envName);
  },
});

Measuring Platform Success

Platform teams often struggle to demonstrate value. Track these metrics:

DORA Metrics via Catalog

// plugins/dora-metrics/src/api.ts
export async function getDoraMetrics(serviceName: string, days: number = 30) {
  const [deployments, incidents, prs] = await Promise.all([
    fetchDeployments(serviceName, days),
    fetchIncidents(serviceName, days),
    fetchMergedPRs(serviceName, days),
  ]);
  
  const deploymentFrequency = deployments.length / days;
  const changeLeadTime = median(prs.map(pr => pr.mergeTime - pr.firstCommitTime));
  const changeFailureRate = incidents.filter(i => i.causedByDeploy).length / deployments.length;
  const mttr = median(incidents.map(i => i.resolvedAt - i.createdAt));
  
  return { deploymentFrequency, changeLeadTime, changeFailureRate, mttr };
}

Developer Satisfaction

Run automated pulse surveys through Backstage. Track:

  • Time to first production deployment (new hire onboarding)
  • Self-service rate (% of tasks completed without platform team help)
  • Platform NPS score

Real-World ROI

Data from teams that invested in IDPs (2025 DORA Report):

  • 45% faster onboarding — New engineers deploy to production in days, not weeks
  • 60% reduction in ops tickets — Developers self-serve instead of waiting
  • 3x deployment frequency — Golden paths make deploying safe and easy
  • 35% fewer production incidents — Platform-enforced standards catch problems early

Getting Started: The Platform Team’s Roadmap

Month 1-2: Foundation

  • Deploy Backstage with GitHub/GitLab integration
  • Import your existing services into the catalog
  • Add TechDocs for documentation

Month 3-4: Golden Paths

  • Build 2-3 software templates for your most common service types
  • Integrate CI/CD pipeline visibility
  • Add Kubernetes status to entity pages

Month 5-6: Self-Service

  • Environment provisioning via templates
  • Database provisioning automation
  • Secrets management integration

Month 7+: Advanced

  • Custom plugins for internal systems
  • DORA metrics dashboard
  • Developer productivity analytics

Conclusion

Platform Engineering is the acknowledgment that “full stack developer who also manages infrastructure” doesn’t scale. Organizations that invest in IDPs — and Backstage is the best open-source option in 2026 — consistently out-ship and out-recruit competitors.

The key insight: developers shouldn’t need to understand Kubernetes to deploy to Kubernetes. The platform team’s job is to make the right thing the easy thing.

Start small. Build one template that works really well. Get developer feedback. Iterate. The platform will pay for itself many times over.

Resources:


이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)