Skip to content
DevOps Architect

    Syllabus / Delivery / 16

    INTERNAL DEVELOPER PLATFORM
    16 / 19 • PLATFORM ENGINEERING

    🏗Platform Engineering & IDP

    The Senior DevOps Engineer of 2026 is a Platform Engineer. Build Internal Developer Platforms that abstract cloud complexity so 200 developers can self-serve. Master Backstage catalog, Crossplane XRDs for database-as-a-service, golden-path scaffolding, and DORA metrics dashboards. Reduce Mean Time to Onboard from 2 weeks to 2 hours.

    Backstage Catalog Crossplane XRDs Golden Paths + DORA

     The Problem: Developer Toil at Scale

    Before Platform Engineering at a 300-person engineering org:

    • New developer onboarding: 2–3 weeks of ticket-driven infra setup
    • Creating a new microservice: 5 Jira tickets to DevOps team (ECR repo, IAM role, K8s namespace, Vault path, DNS entry)
    • DevOps team: 70% of time on toil, 30% on actual platform work
    • Shadow IT: devs spinning up EC2s directly to avoid the process

    After IDP with Backstage + Crossplane: New service scaffold = 1 PR merge → 15 minutes → everything provisioned.

    IDP Architecture 16.1

    graph TD subgraph "Developer Experience" Dev[Developer] CLI[Backstage CLI / UI] PR[GitHub PR] end subgraph "IDP Control Plane" Backstage[Backstage
    Portal + Catalog + TechDocs] Templates[Software Templates
    Golden Paths] ArgoCD[ArgoCD
    GitOps Controller] end subgraph "Infrastructure Plane" Crossplane[Crossplane
    XRDs for Cloud Resources] TFC[Terraform Cloud
    Complex IaC Modules] Vault[HashiCorp Vault
    Dynamic Secrets] end subgraph "Cloud Resources" RDS[RDS PostgreSQL] S3[S3 Buckets] ECR[ECR Registry] NS[K8s Namespace + RBAC] end Dev -->|creates service| CLI CLI -->|scaffold template| Templates Templates -->|opens PR| PR PR -->|merge triggers| ArgoCD ArgoCD -->|reconciles| Crossplane Crossplane --> RDS Crossplane --> S3 Crossplane --> NS Backstage -->|reads catalog| ArgoCD

    Backstage: Software Catalog & Golden Paths 16.2

    Backstage is Spotify's open-source IDP that became the industry standard. It provides: (1) a Software Catalog — every service, API, team, and resource in one searchable UI; (2) Software Templates — interactive scaffolding wizards for developers; (3) TechDocs — docs-as-code rendered from markdown in Git.

    catalog-info.yaml (every repo has this)
    apiVersion: backstage.io/v1alpha1
    kind: Component
    metadata:
      name: payments-api
      description: Payment processing microservice
      annotations:
        # Link to GitHub repo, Datadog dashboard, PagerDuty service
        github.com/project-slug: acme-corp/payments-api
        pagerduty.com/integration-key: abc123def456
        grafana/dashboard-url: https://grafana.acme.com/d/payments
        backstage.io/techdocs-ref: dir:.
      labels:
        language: python
        domain: payments
        tier: critical
      tags:
        - python
        - fastapi
        - kafka
        - postgresql
    spec:
      type: service
      lifecycle: production
      owner: group:payments-team
      system: payment-platform
      dependsOn:
        - resource:postgres-payments-db
        - component:notification-service
      providesApis:
        - payments-v2-openapi
    ---
    apiVersion: backstage.io/v1alpha1
    kind: Resource
    metadata:
      name: postgres-payments-db
      description: PostgreSQL RDS for payments service
      annotations:
        aws.amazon.com/arn: arn:aws:rds:us-east-1:123456:db:payments-prod
    spec:
      type: database
      owner: group:platform-team
      system: payment-platform
    template.yaml (golden path: new microservice)
    apiVersion: scaffolder.backstage.io/v1beta3
    kind: Template
    metadata:
      name: python-fastapi-service
      title: Python FastAPI Microservice
      description: Creates a production-ready FastAPI service with CI/CD, monitoring, and DB
      tags: [python, fastapi, recommended]
    spec:
      owner: group:platform-team
      type: service
    
      parameters:
        - title: Service Details
          required: [serviceName, description, owner]
          properties:
            serviceName:
              type: string
              pattern: '^[a-z][a-z0-9-]{2,30}$'
              description: Lowercase, hyphens only (e.g. order-processor)
            description:
              type: string
            owner:
              type: string
              ui:field: OwnerPicker
    
        - title: Infrastructure
          properties:
            needsDatabase:
              type: boolean
              default: false
              title: PostgreSQL Database?
            dbSize:
              type: string
              enum: [db.t3.micro, db.t3.small, db.m5.large]
              default: db.t3.micro
              ui:widget: radio
              ui:options:
                enumNames: ['Micro (dev/staging)', 'Small (SME prod)', 'Large (enterprise prod)']
            needsKafka:
              type: boolean
              default: false
              title: Kafka Topics?
    
      steps:
        - id: fetch-template
          name: Fetch Base Template
          action: fetch:template
          input:
            url: ./skeleton
            values:
              serviceName: ${{ parameters.serviceName }}
              owner: ${{ parameters.owner }}
    
        - id: create-github-repo
          name: Create GitHub Repository
          action: github:repo:create
          input:
            repoUrl: github.com?owner=acme-corp&repo=${{ parameters.serviceName }}
    
        - id: provision-infrastructure
          name: Provision Cloud Infrastructure
          action: http:backstage:request
          input:
            method: POST
            path: /api/proxy/crossplane
            body:
              serviceName: ${{ parameters.serviceName }}
              needsDatabase: ${{ parameters.needsDatabase }}
              dbSize: ${{ parameters.dbSize }}
    
        - id: register-catalog
          name: Register in Catalog
          action: catalog:register
          input:
            repoContentsUrl: ${{ steps['create-github-repo'].output.repoContentsUrl }}
    
      output:
        links:
          - title: GitHub Repository
            url: ${{ steps['create-github-repo'].output.remoteUrl }}
          - title: Backstage Catalog Entry
            icon: catalog
            entityRef: ${{ steps['register-catalog'].output.entityRef }}
    mkdocs.yml (TechDocs)
    site_name: 'Payments API'
    site_description: 'Technical documentation for the payments service'
    
    plugins:
      - techdocs-core
    
    nav:
      - Home: index.md
      - Architecture:
        - Overview: architecture/overview.md
        - Data Flow: architecture/data-flow.md
        - Dependencies: architecture/dependencies.md
      - Runbooks:
        - On-Call Guide: runbooks/on-call.md
        - Database Recovery: runbooks/db-recovery.md
        - Rollback Procedure: runbooks/rollback.md
      - API Reference: api.md
      - ADRs:
        - ADR-001 Chose FastAPI: adrs/001-fastapi.md
        - ADR-002 Chose PostgreSQL: adrs/002-postgresql.md
    # Create new Backstage app
    $ npx @backstage/create-app@latest --skip-install
    $ cd my-backstage-app && yarn install
    
    # Configure GitHub auth in app-config.yaml
    $ cat > app-config.local.yaml << 'EOF'
    auth:
      providers:
        github:
          development:
            clientId: ${GITHUB_CLIENT_ID}
            clientSecret: ${GITHUB_CLIENT_SECRET}
    integrations:
      github:
        - host: github.com
          token: ${GITHUB_TOKEN}
    catalog:
      locations:
        - type: github-org
          target: https://github.com/acme-corp
    EOF
    
    # Start dev server
    $ yarn dev
    app started at http://localhost:3000
    backend started at http://localhost:7007

    Crossplane: Database-as-a-Service 16.3

    Crossplane extends Kubernetes with Composite Resource Definitions (XRDs) — platform teams define abstract APIs (e.g., "I want a PostgreSQL database") and Crossplane provisions the actual cloud resources. Developers never touch AWS console.

    xrd-postgresql.yaml (platform team writes this once)
    # Platform team defines the abstract API
    apiVersion: apiextensions.crossplane.io/v1
    kind: CompositeResourceDefinition
    metadata:
      name: xpostgresqlinstances.database.acme.io
    spec:
      group: database.acme.io
      names:
        kind: XPostgreSQLInstance
        plural: xpostgresqlinstances
      claimNames:
        kind: PostgreSQLInstance     # What developers use
        plural: postgresqlinstances
      versions:
        - name: v1alpha1
          served: true
          referenceable: true
          schema:
            openAPIV3Schema:
              type: object
              properties:
                spec:
                  type: object
                  required: [parameters]
                  properties:
                    parameters:
                      type: object
                      required: [size, team]
                      properties:
                        size:
                          type: string
                          enum: [small, medium, large]
                          description: "small=t3.micro, medium=m5.large, large=m5.2xlarge"
                        team:
                          type: string
                          description: "Team name for cost tagging"
                        version:
                          type: string
                          default: "15.4"
                        multiAZ:
                          type: boolean
                          default: false
    composition-rds.yaml (platform team writes this once)
    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: postgresql-aws
    spec:
      compositeTypeRef:
        apiVersion: database.acme.io/v1alpha1
        kind: XPostgreSQLInstance
      resources:
        - name: rds-instance
          base:
            apiVersion: rds.aws.upbound.io/v1beta1
            kind: Instance
            spec:
              forProvider:
                region: us-east-1
                engine: postgres
                autoMinorVersionUpgrade: true
                backupRetentionPeriod: 7
                storageEncrypted: true
                deletionProtection: true
                vpcSecurityGroupIdSelector:
                  matchLabels:
                    usage: rds
          patches:
            # Map size enum to actual instance type
            - type: CombineFromComposite
              combine:
                variables:
                  - fromFieldPath: spec.parameters.size
                strategy: string
                string:
                  fmt: |
                    %[1]s
              transforms:
                - type: map
                  map:
                    small: db.t3.micro
                    medium: db.m5.large
                    large: db.m5.2xlarge
              toFieldPath: spec.forProvider.instanceClass
            - fromFieldPath: spec.parameters.version
              toFieldPath: spec.forProvider.engineVersion
            - fromFieldPath: spec.parameters.multiAZ
              toFieldPath: spec.forProvider.multiAZ
            - fromFieldPath: spec.parameters.team
              toFieldPath: spec.forProvider.tags.Team
    
        - name: rds-password-secret
          base:
            apiVersion: secretsmanager.aws.upbound.io/v1beta1
            kind: Secret
            spec:
              forProvider:
                region: us-east-1
                recoveryWindowInDays: 7
    my-db-claim.yaml (developer writes this — no AWS knowledge needed)
    # Developer just asks for a "PostgreSQL database"
    # No knowledge of RDS, VPCs, subnets, security groups needed
    apiVersion: database.acme.io/v1alpha1
    kind: PostgreSQLInstance
    metadata:
      name: payments-db
      namespace: payments-team    # Lives in their own namespace
    spec:
      parameters:
        size: medium              # Platform team decided what "medium" means
        team: payments
        version: "15.4"
        multiAZ: true
      writeConnectionSecretToRef:
        name: payments-db-creds   # K8s Secret with host/user/password
    ---
    # Developer then uses the connection secret in their Deployment
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: payments-api
    spec:
      template:
        spec:
          containers:
            - name: payments-api
              envFrom:
                - secretRef:
                    name: payments-db-creds   # Auto-populated by Crossplane

    DORA Metrics Dashboard 16.4

    Elite Performer Targets

    MetricEliteHigh
    Deployment Frequency>1/day1/week
    Lead Time for Changes<1 hour<1 week
    MTTR<1 hour<1 day
    Change Failure Rate<5%<10%

    Collecting DORA via GitHub API

    # Deployment frequency from GitHub deployments
    gh api repos/acme-corp/payments-api/deployments \
      --paginate \
      --jq '[.[] | select(.environment=="production")] | length'
    
    # Lead time: PR merge → deployment
    gh api repos/acme-corp/payments-api/pulls \
      --paginate \
      --jq '.[] | {
        number: .number,
        merged: .merged_at,
        created: .created_at
      }'

    Hands-On Labs 16.5

    Platform Engineering Labs

    • Bootstrap Backstage locally with yarn, add GitHub integration, register 3 catalog entries
    • Create a Software Template that scaffolds a Python service repo with CI/CD workflow
    • Install Crossplane on kind, install AWS provider, create an XRD for S3 buckets
    • Write a Crossplane Composition that provisions a K8s Namespace + RBAC + ResourceQuota as a bundle
    • Build a DORA metrics script pulling deployment frequency from GitHub API over 30 days
    • Set up TechDocs for a sample service — write runbook, architecture doc, and ADR pages

    Troubleshooting 16.6

    Check kubectl describe compositepgresqlinstance payments-db for events. Common causes: AWS provider credentials not configured (kubectl describe providerconfig aws), Composition not matching the XR's compositeTypeRef, or IRSA role missing required IAM permissions.

    Check GitHub token scopes — needs repo and read:org. Verify catalog-info.yaml exists at repo root. Check Backstage backend logs for processor errors. Ensure the file is valid YAML with correct apiVersion.

    Scaffolder actions run in Backstage backend. Check backend logs. github:repo:create needs a GitHub app or PAT with repo creation permission. Use the Backstage dry-run mode (--dry-run) to test templates locally.

    Run: npx @backstage/create-app@latest --skip-install

    Extra commands from this lesson (17) are kept out of this page. Quizzes were not in the source HTML.