Platform Engineering with Internal Developer Platforms: Building Self-Service Infrastructure

How Internal Developer Platforms (IDPs) are transforming DevOps teams into platform engineering organizations, reducing cognitive load, and accelerating software delivery through self-service golden paths.

Here’s a conversation I’ve had at least a dozen times this year: “Our developers are drowning in YAML. They wanted to ship features, but instead they’re debugging Kubernetes manifests and writing Terraform modules.” Sound familiar?

This isn’t a developer problem—it’s a platform problem. And the solution isn’t more documentation or better training. It’s fundamentally rethinking how we expose infrastructure to application teams. That’s where Internal Developer Platforms (IDPs) come in.

The Platform Engineering Shift

Let me be clear about what’s happening in the industry. We’re seeing a massive shift from “DevOps teams” to “platform engineering teams.” This isn’t just rebranding—it’s a fundamental change in how we think about infrastructure.

The old model:

  • DevOps team acts as a ticket-taking service
  • Developers open Jira tickets: “Need a new database”, “Deploy to production”, “Create a new environment”
  • DevOps engineers become bottlenecks, spending 80% of their time on repetitive requests
  • Lead time for simple changes: days or weeks

The new model:

  • Platform team builds self-service capabilities
  • Developers provision their own infrastructure through abstracted interfaces
  • Platform team focuses on building reliable, scalable platforms
  • Lead time for simple changes: minutes or hours

I’ve seen teams cut their deployment lead time from 3 weeks to 30 minutes by implementing a proper IDP. That’s not hype—that’s removing friction from the development process.

What Makes a Great Internal Developer Platform?

An IDP isn’t a single tool—it’s a collection of integrated capabilities that abstract away infrastructure complexity while maintaining flexibility. Here’s what I’ve found to be essential:

  • Service catalog: Pre-approved patterns for common needs (databases, caches, message queues)
  • Self-service provisioning: Developers create resources without opening tickets
  • Golden paths: Opinionated, well-tested paths for common workflows
  • Environment management: Ephemeral environments, production parity, automated cleanup
  • Guardrails, not gates: Security and compliance baked in, not bolted on
  • Developer portal: Single pane of glass for discovering capabilities and monitoring services

IDP Architecture on Azure

Let me show you a real architecture I’ve implemented for a client running on Azure. This IDP serves 150+ developers across 30 microservices:

graph TB
    subgraph DevPortal["Developer Portal (Backstage)"]
        Catalog["Service Catalog
Templates & Docs"] UI["Self-Service UI"] end subgraph ControlPlane["Control Plane"] Crossplane["Crossplane
Universal Control Plane"] ArgoCD["ArgoCD
GitOps Engine"] FluxCD["FluxCD
Cluster Config"] end subgraph AzureInfra["Azure Infrastructure"] AKS["AKS Clusters
(Dev, Stage, Prod)"] ACDB["Azure Cosmos DB"] ACR["Azure Container Registry"] KeyVault["Azure Key Vault"] Monitor["Azure Monitor"] end subgraph Git["Git Repository"] AppCode["Application Code"] InfraCode["Infrastructure as Code
(Crossplane Manifests)"] Config["Environment Config"] end UI -->|"Create Service"| Catalog Catalog -->|"Generate from template"| Git Git -->|"Watch"| ArgoCD Git -->|"Watch"| FluxCD ArgoCD -->|"Deploy workloads"| AKS FluxCD -->|"Configure cluster"| AKS Crossplane -->|"Provision resources"| AzureInfra AKS -->|"Use"| ACDB AKS -->|"Pull images"| ACR AKS -->|"Fetch secrets"| KeyVault AKS -->|"Send metrics"| Monitor style DevPortal fill:#e1f5ff style ControlPlane fill:#d4edda style AzureInfra fill:#fff3cd

This architecture gives developers self-service capabilities while maintaining centralized control over security, compliance, and cost management.

Building Block 1: Backstage as the Developer Portal

Backstage (from Spotify) has become the de facto standard for developer portals. I’ve deployed it across multiple organizations, and here’s why it works:

Install Backstage with Azure AD Integration

# Create Backstage app
npx @backstage/create-app@latest

cd backstage-app

# Install Azure AD auth plugin
yarn add --cwd packages/app @backstage/plugin-auth-backend
yarn add --cwd packages/app @backstage/plugin-auth-azure-oauth2-provider

# Configure app-config.yaml
cat <<EOF >> app-config.yaml
auth:
  environment: production
  providers:
    azureAd:
      development:
        clientId: \${AZURE_CLIENT_ID}
        clientSecret: \${AZURE_CLIENT_SECRET}
        tenantId: \${AZURE_TENANT_ID}
EOF

# Start Backstage
yarn dev

Create a Service Template

This is where the magic happens. Templates let developers spin up new services with all the boilerplate pre-configured:

# templates/nodejs-microservice/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: nodejs-microservice
  title: Node.js Microservice
  description: Create a new Node.js microservice with AKS deployment
spec:
  owner: platform-team
  type: service
  parameters:
    - title: Service Information
      required:
        - name
        - owner
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z0-9-]+$'
        owner:
          title: Team
          type: string
          ui:field: OwnerPicker
        description:
          title: Description
          type: string

  steps:
    - id: fetch-base
      name: Fetch Base Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}

    - id: publish
      name: Publish to GitHub
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}

    - id: register
      name: Register Component
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'

When a developer fills out this form in Backstage, they get:

  • New GitHub repository with CI/CD configured
  • Kubernetes manifests for AKS deployment
  • Monitoring dashboards pre-configured
  • Documentation site scaffolded
  • Team ownership auto-assigned

All in under 60 seconds.

Building Block 2: Crossplane for Infrastructure Provisioning

Crossplane turns Kubernetes into a universal control plane for cloud resources. Instead of writing Terraform or ARM templates, developers create Kubernetes manifests, and Crossplane provisions the actual Azure resources.

Install Crossplane on AKS

# Add Crossplane Helm repo
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

# Install Crossplane
helm install crossplane \
  crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --wait

# Install Azure provider
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-azure
spec:
  package: xpkg.upbound.io/upbound/provider-azure:v0.39.0
EOF

# Wait for provider to be healthy
kubectl get providers

Configure Azure Credentials

# Create Azure service principal
az ad sp create-for-rbac \
  --name crossplane-sp \
  --role Contributor \
  --scopes /subscriptions/<subscription-id>

# Create Kubernetes secret
kubectl create secret generic azure-creds \
  -n crossplane-system \
  --from-literal=credentials='{"clientId":"...","clientSecret":"...","subscriptionId":"...","tenantId":"..."}'

# Configure provider
kubectl apply -f - <<EOF
apiVersion: azure.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      name: azure-creds
      namespace: crossplane-system
      key: credentials
EOF

Create a Composite Resource Definition

This is how you create high-level abstractions for developers. Instead of understanding Azure Cosmos DB configuration details, they just request a “database”:

# compositions/database-composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database
    plural: databases
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  size:
                    type: string
                    enum: [small, medium, large]
                  backup:
                    type: boolean
                required:
                  - size
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: azure-cosmosdb
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  resources:
  - name: cosmosdb-account
    base:
      apiVersion: cosmosdb.azure.upbound.io/v1beta1
      kind: Account
      spec:
        forProvider:
          location: East US
          resourceGroupName: platform-resources
          offerType: Standard
          kind: GlobalDocumentDB
          consistencyPolicy:
            - consistencyLevel: Session
    patches:
    - fromFieldPath: "spec.parameters.size"
      toFieldPath: "spec.forProvider.capabilities[0].name"
      transforms:
      - type: map
        map:
          small: EnableServerless
          medium: ""
          large: ""

Now developers can create a database like this:

apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
  name: my-app-database
  namespace: my-team
spec:
  parameters:
    size: medium
    backup: true

Crossplane handles the rest—provisioning the Cosmos DB account, configuring backups, creating connection secrets, and storing them in Key Vault.

Building Block 3: GitOps with ArgoCD

ArgoCD ensures everything deployed to Kubernetes comes from Git—no manual kubectl applies, no undocumented changes. I’ve seen organizations go from “config drift disasters” to “git is the source of truth” in months.

Deploy ArgoCD on AKS

# Create namespace
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for pods to be ready
kubectl wait --for=condition=available --timeout=300s \
  deployment/argocd-server -n argocd

# Get admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

# Port-forward UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Configure Application of Applications Pattern

This pattern lets platform teams manage hundreds of applications from a single root application:

# argocd/root-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/platform-config
    targetRevision: main
    path: argocd/applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Each team’s applications are defined in individual files:

# argocd/applications/team-alpha.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: team-alpha-services
  namespace: argocd
spec:
  project: team-alpha
  source:
    repoURL: https://github.com/myorg/team-alpha-services
    targetRevision: main
    path: kubernetes
  destination:
    server: https://kubernetes.default.svc
    namespace: team-alpha
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Developers push to their repo, ArgoCD syncs automatically. No manual deployments, no “works on my machine” issues.

Real-World Impact: Before and After IDP

Let me share numbers from a recent client engagement. They’re a SaaS company with 80 developers building a multi-tenant platform on AKS.

Before IDP (Traditional DevOps):

  • New service setup: 2-3 weeks (waiting for DevOps team)
  • Deploy to production: 2-4 days (manual approval process)
  • Environment provisioning: 1-2 weeks
  • Developer satisfaction: 3.2/10 (annual survey)
  • Platform team size: 8 engineers (constantly overwhelmed)
  • Production incidents caused by config drift: ~15/month

After IDP (6 months post-implementation):

  • New service setup: 30 minutes (self-service template)
  • Deploy to production: 15 minutes (automated GitOps)
  • Environment provisioning: 5 minutes (Crossplane automation)
  • Developer satisfaction: 8.7/10
  • Platform team size: 6 engineers (focused on platform improvements)
  • Production incidents: ~2/month (git-based auditing + automated testing)

The productivity gains were staggering. Developers went from spending 30% of their time on infrastructure to less than 5%. The platform team evolved from firefighting to strategic work—improving observability, optimizing costs, and building new platform capabilities.

Common Pitfalls I’ve Seen

Building an IDP isn’t without challenges. Here’s what trips up most teams:

Don’t do this:

  • Build everything at once—start with one use case, prove value, expand
  • Create too many abstractions—golden paths should be opinionated but not rigid
  • Ignore developer feedback—they’re your customers, not your users
  • Skip documentation—self-service only works if people know what’s available
  • Forget about Day 2 operations—provisioning is easy, managing is hard

Do this instead:

  • Start with the most painful developer workflow (usually new service creation)
  • Co-create with development teams—they’ll tell you what they need
  • Make the golden path the easy path, not the only path
  • Instrument everything—you need metrics to prove platform value
  • Plan for progressive delivery—feature flags, canary deployments, rollback capabilities

The Developer Experience Loop

Here’s what a complete developer workflow looks like on a mature IDP:

graph LR
    A[Developer has idea] -->|"Use Backstage template"| B[Service scaffolded]
    B -->|"Git push"| C[CI pipeline runs]
    C -->|"Tests pass"| D[ArgoCD deploys to dev]
    D -->|"Developer validates"| E[Promote to staging]
    E -->|"Automated tests pass"| F[Deploy to production]
    F -->|"Monitor with Grafana"| G[Service running]
    G -->|"Issue detected"| H[Alerts fire]
    H -->|"Rollback or fix"| C

    style A fill:#e1f5ff
    style G fill:#d4edda
    style H fill:#f8d7da

From idea to production: 1-2 hours. That’s the power of a well-designed platform.

Key Takeaways

  • Platform engineering is about building capabilities, not taking tickets—shift from service to product mindset
  • IDPs reduce cognitive load—developers focus on business logic, not YAML and cloud APIs
  • Self-service doesn’t mean no guardrails—bake security and compliance into the platform
  • Start small and iterate—one golden path is better than a half-built platform
  • Measure everything—track lead time, deployment frequency, and developer satisfaction
  • Backstage + Crossplane + ArgoCD is a proven stack for Azure-based platforms
  • Developer experience is a competitive advantage—fast, happy developers ship better products

The organizations winning right now aren’t the ones with the most DevOps engineers—they’re the ones with the best platforms. If you’re still running infrastructure teams like IT helpdesks, you’re falling behind. Build the platform. Empower developers. Watch velocity skyrocket.


Building an IDP for your organization? I’ve done this multiple times and would be happy to discuss your specific platform requirements and help you avoid the mistakes I’ve made along the way.