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.