Admin

DevOps

Platform Engineering in 2026: Building Internal Developer Platforms with Backstage and Crossplane [Strategy]

Design and implement an Internal Developer Platform using Backstage for developer portal, Crossplane for infrastructure abstraction, and ArgoCD for delivery.

By Sujay SinghPublished: June 8, 202611 min read12 views✓ Fact Checked
Platform Engineering in 2026: Building Internal Developer Platforms with Backstage and Crossplane [Strategy]
Platform Engineering in 2026: Building Internal Developer Platforms with Backstage and Crossplane [Strategy]

Platform Engineering in 2026: Building Internal Developer Platforms with Backstage and Crossplane [Strategy]

As Sujay Singh, a senior technology writer at TechNews Venture, I’ve tracked the evolution of software development methodologies for over a decade. The industry's relentless pursuit of efficiency, scalability, and developer satisfaction has brought us to a pivotal point: the widespread adoption of Platform Engineering. By 2026, the concept of an Internal Developer Platform (IDP) will no longer be a niche aspiration but a foundational component for high-performing technology organizations. This article will delve into a strategic approach for building a robust IDP using two leading open-source projects: Backstage for the developer experience and Crossplane for infrastructure orchestration.

Overview: The Rise of Internal Developer Platforms

The promise of DevOps was to bridge the chasm between development and operations. While it brought significant cultural shifts and automation, it often pushed operational complexities onto developers, leading to cognitive overload and slower innovation cycles. Platform Engineering addresses this by providing "golden paths"—curated, opinionated, and automated frameworks that abstract away underlying infrastructure complexities, allowing developers to focus purely on application logic.

An Internal Developer Platform (IDP) is the tangible embodiment of Platform Engineering. It's a self-service portal that offers developers everything they need to build, deploy, and operate their applications, from spinning up new microservices and provisioning databases to accessing logs and metrics. By 2026, a mature IDP will be characterized by:

  • Self-Service Empowerment: Developers provision resources and deploy applications without direct ops intervention.
  • Standardization: Enforced best practices for security, reliability, and cost-efficiency.
  • Abstraction: Hiding the underlying complexity of cloud providers and Kubernetes.
  • Observability: Integrated tooling for monitoring, logging, and tracing.
  • Developer Experience (DX): A delightful, intuitive interface that enhances productivity.

In this strategic blueprint, Backstage, Spotify's open-source developer portal, serves as the primary interface for developers. It provides a unified catalog of services, software templates, documentation, and operational insights. Complementing Backstage, Crossplane acts as the universal control plane, extending Kubernetes to manage and provision external cloud infrastructure (databases, message queues, object storage, etc.) declaratively. This powerful combination delivers a seamless, GitOps-driven IDP where developers interact with Backstage, and Backstage orchestrates infrastructure via Crossplane.

Prerequisites for Implementation

Before embarking on building your IDP, ensure the following foundational components and knowledge are in place:

  • Kubernetes Cluster: A running Kubernetes cluster (e.g., AWS EKS, GCP GKE, Azure AKS) is essential. Both Backstage and Crossplane will be deployed here.
  • kubectl: The Kubernetes command-line tool, configured to connect to your cluster.
  • Helm: The Kubernetes package manager, used for deploying Crossplane and potentially Backstage.
  • Node.js and Yarn: Required for developing and running Backstage locally.
  • Git Repository: A version control system (GitHub, GitLab, Bitbucket) for storing Backstage configurations, Crossplane Compositions, and software templates.
  • Cloud Provider Account: An AWS, Azure, or GCP account with programmatic access (API keys or IAM roles) for Crossplane to manage resources. For this article, we'll primarily use AWS examples.
  • Basic Understanding: Familiarity with Kubernetes concepts, YAML syntax, GitOps principles, and cloud infrastructure.

Detailed Steps: Building the IDP Core

Step 1: Setting up Crossplane as the Infrastructure Control Plane

Crossplane extends your Kubernetes API to allow you to provision and manage cloud infrastructure using kubectl. This is the engine that drives your infrastructure requests from Backstage.

1.1 Install Crossplane into your Kubernetes Cluster

First, add the Crossplane Helm repository and install it:


helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace --wait
1.2 Install a Cloud Provider (e.g., AWS Provider)

Next, install the Crossplane AWS Provider. This allows Crossplane to interact with AWS APIs.


kubectl crossplane install provider crossplane/provider-aws:v0.40.0
kubectl get providers

Wait until the provider status shows `HEALTHY`.

1.3 Configure AWS Credentials for Crossplane

Crossplane needs credentials to interact with your AWS account. The most secure way is to use Kubernetes Service Accounts with IAM Roles for Service Accounts (IRSA) on EKS, or similar mechanisms on GKE/AKS. For demonstration, we'll use a secret.

Create an AWS IAM user with programmatic access and sufficient permissions (e.g., `AdministratorAccess` for a demo, but restrict strictly in production). Store the credentials as a Kubernetes Secret:


# Replace YOUR_AWS_ACCESS_KEY_ID and YOUR_AWS_SECRET_ACCESS_KEY
cat <

Then, create a `ProviderConfig` to reference these credentials:


cat <
1.4 Define a Composite Resource (XR) and Composition for an RDS Database

This is where Crossplane shines. We define an abstract `XPostgreSQLInstance` that developers can request, and a `Composition` that maps this abstract request to concrete AWS RDS resources.


# xrds.yaml - Composite Resource Definition (XRD)
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.platform.example.org
spec:
  group: platform.example.org
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  claimNames:
    kind: PostgreSQLInstance
    plural: postgresqlinstances
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    storageGB:
                      type: integer
                      description: "Storage in GB for the PostgreSQL instance."
                    engineVersion:
                      type: string
                      description: "PostgreSQL engine version."
                    instanceType:
                      type: string
                      description: "AWS RDS instance type."
                  required: ["storageGB", "engineVersion", "instanceType"]
              required: ["parameters"]
            status:
              type: object
              properties:
                rdsEndpoint:
                  type: string
                rdsPort:
                  type: string
---
# composition.yaml - Composition for XPostgreSQLInstance
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xpostgresqlinstances.platform.example.org
  labels:
    provider: aws
    db: postgresql
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: XPostgreSQLInstance
  resources:
    - name: rdsinstance
      base:
        apiVersion: rds.aws.crossplane.io/v1beta1
        kind: DBInstance
        spec:
          forProvider:
            region: us-east-1
            dbSubnetGroupNameRef:
              name: default-vpc-dbsubnetgroup # Ensure this exists or create one
            masterUsername: masteruser
            engine: postgres
            engineVersion: "13.7"
            skipFinalSnapshotBeforeDeletion: true
            publiclyAccessible: false
            vpcSecurityGroupIDs:
              - sg-0a1b2c3d4e5f6g7h8 # Replace with an actual security group ID
          writeConnectionSecretToRef:
            namespace: crossplane-system
            name: rds-conn-secret
      patches:
        - fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.forProvider.allocatedStorage
        - fromFieldPath: spec.parameters.engineVersion
          toFieldPath: spec.forProvider.engineVersion
        - fromFieldPath: spec.parameters.instanceType
          toFieldPath: spec.forProvider.dbInstanceClass
        - fromFieldPath: status.atProvider.endpoint.address
          toFieldPath: status.rdsEndpoint
        - fromFieldPath: status.atProvider.endpoint.port
          toFieldPath: status.rdsPort

kubectl apply -f xrds.yaml
kubectl apply -f composition.yaml

Now, a developer can request a `PostgreSQLInstance` without knowing the underlying AWS RDS complexities.

Step 2: Deploying Backstage as the Developer Portal

Backstage will be the primary interface where developers interact with the platform. It provides the service catalog, software templates, and potentially operational dashboards.

2.1 Initialize a Backstage Application

On your local machine, create a new Backstage app. This sets up the basic structure.


npx @backstage/cli create-app --path my-idp-backstage
cd my-idp-backstage
yarn install
2.2 Configure Backstage for Database and Authentication

Edit `app-config.yaml` to point to a PostgreSQL database (recommended for production) and configure an authentication provider (e.g., GitHub OAuth).

Database Configuration Example (PostgreSQL):


# app-config.yaml
backend:
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: ${POSTGRES_DATABASE}

These environment variables would be supplied to the Backstage deployment in Kubernetes.

GitHub Auth Provider Example:


# app-config.yaml
auth:
  environment: development
  providers:
    github:
      development:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

You'll need to register a new OAuth application in GitHub and get the client ID and secret.

2.3 Dockerize and Deploy Backstage to Kubernetes

Build a Docker image for your Backstage app:


# In your my-idp-backstage directory
yarn build:backend
docker build -t my-backstage-app:latest .
docker push my-backstage-app:latest # Push to your container registry

Deploy Backstage to Kubernetes using Helm (or Kustomize). A simplified example using a generic Helm chart:


# backstage-values.yaml
image:
  repository: YOUR_CONTAINER_REGISTRY/my-backstage-app
  tag: latest
env:
  - name: POSTGRES_HOST
    value: "my-postgres-service.default.svc.cluster.local" # Or external RDS endpoint
  - name: POSTGRES_PORT
    value: "5432"
  - name: POSTGRES_USER
    valueFrom:
      secretKeyRef:
        name: backstage-db-creds
        key: user
  - name: POSTGRES_PASSWORD
    valueFrom:
      secretKeyRef:
        name: backstage-db-creds
        key: password
  - name: POSTGRES_DATABASE
    value: "backstage_db"
  - name: AUTH_GITHUB_CLIENT_ID
    valueFrom:
      secretKeyRef:
        name: github-oauth-creds
        key: client_id
  - name: AUTH_GITHUB_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: github-oauth-creds
        key: client_secret
ingress:
  enabled: true
  className: nginx
  host: backstage.your-domain.com

# Assuming you have a generic Backstage Helm chart or create your own
helm install backstage ./charts/backstage-app -f backstage-values.yaml --namespace backstage-system --create-namespace

Step 3: Integrating Backstage Scaffolder with Crossplane

This is the core of the IDP: enabling developers to provision infrastructure through Backstage templates, which then trigger Crossplane.

3.1 Create a Custom Scaffolder Action for Crossplane

Backstage Scaffolder actions are JavaScript/TypeScript functions that perform tasks. We'll create an action that generates a Crossplane XR (e.g., `PostgreSQLInstance`) and pushes it to a Git repository. A GitOps tool (like Argo CD or Flux CD) will then pick up this YAML and apply it to Kubernetes, triggering Crossplane.

Add a custom action to your Backstage backend:

In `packages/backend/src/plugins/scaffolder.ts`, add a custom action. First, install `simple-git`:


cd packages/backend
yarn add simple-git

Then, modify `scaffolder.ts`:


// packages/backend/src/plugins/scaffolder.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-backend';
import { SimpleGit, simpleGit } from 'simple-git';
import * as yaml from 'js-yaml'; // yarn add js-yaml

export function createCrossplanePostgresAction() {
  return createTemplateAction<{
    resourceName: string;
    gitRepoUrl: string;
    gitBranch: string;
    storageGB: number;
    engineVersion: string;
    instanceType: string;
  }>({
    id: 'platform:crossplane-postgresql',
    schema: {
      input: {
        required: ['resourceName', 'gitRepoUrl', 'gitBranch', 'storageGB', 'engineVersion', 'instanceType'],
        type: 'object',
        properties: {
          resourceName: {
            type: 'string',
            title: 'Resource Name',
            description: 'Name for the PostgreSQL instance.',
          },
          gitRepoUrl: {
            type: 'string',
            title: 'Git Repository URL',
            description: 'URL of the GitOps repository for Crossplane XRs.',
          },
          gitBranch: {
            type: 'string',
            title: 'Git Branch',
            description: 'Branch to push the XR YAML to (e.g., main).',
            default: 'main',
          },
          storageGB: {
            type: 'number',
            title: 'Storage (GB)',
            description: 'Allocated storage for the database.',
          },
          engineVersion: {
            type: 'string',
            title: 'Engine Version',
            description: 'PostgreSQL engine version (e.g., 13.7).',
          },
          instanceType: {
            type: 'string',
            title: 'Instance Type',
            description: 'AWS RDS instance type (e.g., db.t3.micro).',
          },
        },
      },
    },
    async handler(ctx) {
      const { resourceName, gitRepoUrl, gitBranch, storageGB, engineVersion, instanceType } = ctx.input;
      const logger = ctx.logger;

      logger.info(`Generating Crossplane PostgreSQL XR for ${resourceName}`);

      const xrYaml = yaml.dump({
        apiVersion: 'platform.example.org/v1alpha1',
        kind: 'PostgreSQLInstance',
        metadata: {
          name: resourceName,
          labels: {
            'backstage.io/template-id': ctx.templateInfo?.entityRef,
            'backstage.io/scaffolder-task-id': ctx.taskId,
          },
        },
        spec: {
          parameters: {
            storageGB,
            engineVersion,
            instanceType,
          },
        },
      });

      const xrFilePath = `infrastructure/postgres/${resourceName}.yaml`;
      logger.info(`Writing XR to temporary file: ${xrFilePath}`);
      await ctx.create>.fs.writeFile(xrFilePath, xrYaml);

      logger.info(`Cloning Git repository: ${gitRepoUrl}`);
      const git: SimpleGit = simpleGit();
      const repoDir = await ctx.createTemporaryDirectory();
      await git.clone(gitRepoUrl, repoDir);
      await git.cwd(repoDir);
      await git.checkout(gitBranch);

      const targetFilePath = `${repoDir}/${xrFilePath}`;
      logger.info(`Copying generated XR to Git repository: ${targetFilePath}`);
      await ctx.fs.copy(ctx.fs.path(xrFilePath), targetFilePath);

      await git.add(targetFilePath);
      await git.commit(`feat(${resourceName}): Create new PostgreSQL instance via Backstage Scaffolder`);
      await git.push('origin', gitBranch);

      logger.info(`Successfully pushed Crossplane XR to Git repository: ${gitRepoUrl} on branch ${gitBranch}`);
      ctx.output('xrName', resourceName);
    },
  });
}

Then, register this action in `packages/backend/src/index.ts`:


// packages/backend/src/index.ts
// ... other imports
import { createCrossplanePostgresAction } from './plugins/scaffolder';

async function main() {
  // ...
  const scaffolderEnv = use
    .at('/scaffolder')
    .apply(
      ({ services }) =>
        new ScaffolderService(
          {
            logger: services.logger,
            database: services.database,
            reader: services.urlReader,
            config: services.config,
            catalogClient: services.catalog,
            identity: services.identity,
            permissions: services.permissions,
            auth: services.auth,
            // Add custom actions here
            actions: [
              ...defaultActions,
              createCrossplanePostgresAction(),
            ],
          },
        ),
    );
  // ...
}

Note on Git Authentication: For the `simple-git` action to push, the Backstage backend needs Git credentials configured, typically via SSH keys or HTTPS token environment variables. In a production Kubernetes deployment, this would involve mounting secrets containing these credentials.

3.2 Create a Backstage Software Template

Now, define a `template.yaml` that uses this custom action. This template will reside in a Git repository that Backstage monitors.


# templates/new-service-with-rds/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: new-service-with-rds
  title: New Service with PostgreSQL RDS
  description: Creates a new microservice skeleton and provisions an AWS RDS PostgreSQL instance.
  tags:
    - go
    - microservice
    - postgresql
spec:
  owner: platform-team
  type: service
  parameters:
    - title: Service Information
      properties:
        serviceName:
          title: Service Name
          type: string
          description: Unique name for your new service.
          ui:autofocus: true
          ui:options:
            rows: 5
        description:
          title: Description
          type: string
          description: A brief description of your new service.
        owner:
          title: Owner
          type: string
          description: Who is the owner of this service?
          ui:field: OwnerPicker
          ui:options:
            allowedKinds: [ "Group" ]
    - title: Database Configuration
      properties:
        dbName:
          title: Database Name
          type: string
          description: Name for the PostgreSQL database.
          pattern: "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*$"
          maxLength: 63
        dbStorageGB:
          title: Database Storage (GB)
          type: number
          default: 20
          minimum: 10
          maximum: 100
        dbEngineVersion:
          title: Database Engine Version
          type: string
          enum: ["13.7", "14.6", "15.2"]
          default: "13.7"
        dbInstanceType:
          title: Database Instance Type
          type: string
          enum: ["db.t3.micro", "db.t3.small", "db.m5.large"]
          default: "db.t3.micro"
    - title: GitOps Repository
      properties:
        gitOpsRepoUrl:
          title: GitOps Repository URL
          type: string
          description: The URL of your GitOps repository (e.g., for Crossplane XRs).
          default: "https://github.com/your-org/platform-gitops.git" # Replace with your repo
        gitOpsBranch:
          title: GitOps Branch
          type: string
          description: The branch to push Crossplane configurations to.
          default: "main"
  steps:
    - id: fetch-base
      name: Fetch Base Skeleton
      action: fetch:template
      input:
        url: ./skeleton # A subdirectory with basic service code (e.g., Go/Python microservice)
        copyWithoutRender:
          - .github/workflows/*

    - id: generate-service
      name: Generate Service
      action: customize:template
      input:
        files:
          - from: './catalog-info.yaml.hbs'
            to: './catalog-info.yaml'
          - from: './README.md.hbs'
            to: './README.md'

    - id: create-db-xr
      name: Create Crossplane PostgreSQL Instance
      action: platform:crossplane-postgresql
      input:
        resourceName: "{{ parameters.dbName }}"
        gitRepoUrl: "{{ parameters.gitOpsRepoUrl }}"
        gitBranch: "{{ parameters.gitOpsBranch }}"
        storageGB: "{{ parameters.dbStorageGB }}"
        engineVersion: "{{ parameters.dbEngineVersion }}"
        instanceType: "{{ parameters.dbInstanceType }}"

    - id: publish
      name: Publish to Git
      action: publish:github
      input:
        repoUrl: "github.com?owner=your-org&repo={{ parameters.serviceName }}" # Assuming a new repo per service
        defaultBranch: main
        repoVisibility: public # or private

    - id: register
      name: Register in Catalog
      action: catalog:register
      input:
        repoContentsUrl: "https://github.com/your-org/{{ parameters.serviceName }}"
        catalogInfoPath: "/catalog-info.yaml"

Once this template is registered in Backstage (via `app-config.yaml` or a catalog file), developers can navigate to the "Create" tab, select "New Service with PostgreSQL RDS," fill out the form, and Backstage will:

  1. Scaffold the basic service code.
  2. Generate the `PostgreSQLInstance` XR YAML.
  3. Push the XR YAML to the specified GitOps repository.
  4. (Via GitOps controller) Apply the XR to Kubernetes.
  5. Crossplane provisions the AWS RDS instance.
  6. Register the new service in the Backstage catalog.

This creates a powerful, self-service experience, abstracting complex cloud provisioning behind a simple form.

Security Considerations for your IDP

Security is paramount in an IDP, given its central role in both application development and infrastructure management. A compromise here could have wide-ranging implications.

Backstage Security

  • Authentication & Authorization:
    • Integrate with enterprise SSO (e.g., Okta, Azure AD, GitHub OAuth) for robust authentication.
    • Implement Backstage's RBAC (Role-Based Access Control) plugin to control who can view, edit, or create resources and templates. Ensure least privilege.
  • Secrets Management:
    • Never hardcode secrets in `app-config.yaml`. Use environment variables or a secrets manager like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets (encrypted at rest).
  • Supply Chain Security:
    • Regularly scan Backstage dependencies for CVEs using tools like Snyk, Trivy, or Dependabot.
    • Ensure Backstage plugins are from trusted sources or thoroughly vetted if custom-developed.
  • Network Security:
    • Deploy Backstage behind an ingress controller with TLS termination.
    • Implement network policies in Kubernetes to restrict communication between Backstage pods and other services.

Crossplane Security

  • IAM Roles for Service Accounts (IRSA):
    • For cloud providers, use IRSA (AWS EKS), Workload Identity (GCP GKE), or Azure AD Workload Identity (Azure AKS) to grant Crossplane providers least-privilege access to cloud resources. Avoid long-lived access keys in secrets where possible.
    • Example for AWS ProviderConfig with IRSA:
      
      apiVersion: aws.crossplane.io/v1beta1
      kind: ProviderConfig
      metadata:
        name: default
      spec:
        credentials:
          source: IRSA
          fsGroup: 2000 # Recommended for EKS IAM roles for service accounts
          # serviceAccountSelector:
          #   matchLabels:
          #     aws-account-id: "123456789012"
                  
  • Policy Enforcement:
    • Use admission controllers like OPA Gatekeeper or Kyverno to enforce policies on Crossplane resources (XRs and managed resources). For example, prevent creation of publicly accessible databases or enforce specific instance types.
    • Example Gatekeeper Constraint for RDS:
      
      apiVersion: constraints.gatekeeper.sh/v1
📧

Enjoyed this article?

Get articles like this delivered to your inbox daily. Join 10,000+ tech professionals.

Written By

Sujay Singh

Technology Expert / Cloud Architect at Virtual Venture covering AI, cloud computing, cybersecurity, and emerging tech trends.

Sources & References

• Official company announcements and press releases

• Industry reports from Gartner, IDC, and Statista

• Peer-reviewed research and technical documentation

• On-record statements from industry experts

Last verified: June 8, 2026

Fact-checked by TechNews Venture editorial team

Leave a Comment

Comments are moderated and will appear after review.