GitOps & Continuous Delivery
GitOps + Crossplane + Backstage
3 min read
The true power of platform engineering emerges when you integrate Backstage, Crossplane, and GitOps into a unified workflow. This lesson shows how to build an end-to-end self-service platform.
The Complete IDP Architecture
┌─────────────────────────────────────────────────────────┐
│ DEVELOPER PORTAL │
│ (Backstage) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Catalog │ │ Templates │ │ TechDocs │ │
│ └──────────────┘ └──────┬───────┘ └──────────────┘ │
│ │ │
└───────────────────────────┼──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ GIT REPOSITORY │
│ (Source of Truth) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ App Configs │ │ Infra Claims │ │ Rollouts │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
└───────────────────────────┼──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ArgoCD │
│ (GitOps Controller) │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ App Deploy │ │ Crossplane │ │ Rollouts │ │
│ │ (K8s) │ │ Claims │ │ │ │
│ └────────────┘ └─────┬──────┘ └────────────┘ │
│ │ │
└─────────────────────────┼───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ CROSSPLANE │
│ (Infrastructure) │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ AWS │ │ GCP │ │ Azure │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Backstage Template for Full-Stack Service
Create a template that provisions app, infrastructure, and GitOps:
# backstage-template-fullstack.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: create-fullstack-service
title: Create Full-Stack Service
description: |
Creates a new service with:
- Application code repository
- PostgreSQL database (via Crossplane)
- ArgoCD application for GitOps
- Monitoring and alerts
tags:
- recommended
- platform
spec:
owner: platform-team
type: service
parameters:
- title: Service Information
required:
- name
- description
- owner
properties:
name:
title: Service Name
type: string
pattern: '^[a-z][a-z0-9-]*$'
description:
title: Description
type: string
owner:
title: Owner
type: string
ui:field: OwnerPicker
- title: Technical Configuration
properties:
language:
title: Language
type: string
enum: [nodejs, python, go, java]
default: nodejs
framework:
title: Framework
type: string
enum:
- express
- fastapi
- gin
- spring-boot
default: express
- title: Infrastructure
properties:
database:
title: Database Size
type: string
enum: [small, medium, large]
default: small
environment:
title: Environment
type: string
enum: [development, staging, production]
default: development
steps:
# Step 1: Create application repository
- id: create-repo
name: Create Application Repository
action: publish:github
input:
repoUrl: github.com?repo=${{ parameters.name }}&owner=acme
description: ${{ parameters.description }}
defaultBranch: main
repoVisibility: internal
# Step 2: Scaffold application code
- id: scaffold-app
name: Scaffold Application
action: fetch:template
input:
url: ./templates/${{ parameters.language }}-${{ parameters.framework }}
targetPath: ./app
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
# Step 3: Create Crossplane claim for database
- id: create-database
name: Create Database
action: kubernetes:apply
input:
namespaced: true
manifest:
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: ${{ parameters.name }}-db
namespace: ${{ parameters.environment }}
labels:
app: ${{ parameters.name }}
owner: ${{ parameters.owner }}
spec:
size: ${{ parameters.database }}
engine: postgres
writeConnectionSecretToRef:
name: ${{ parameters.name }}-db-credentials
# Step 4: Add to GitOps repository
- id: gitops-pr
name: Create GitOps Configuration
action: publish:github:pull-request
input:
repoUrl: github.com?repo=gitops-repo&owner=acme
branchName: add-${{ parameters.name }}
title: "Add ${{ parameters.name }} to GitOps"
description: |
Adding new service: ${{ parameters.name }}
Owner: ${{ parameters.owner }}
Environment: ${{ parameters.environment }}
targetPath: apps/overlays/${{ parameters.environment }}/${{ parameters.name }}
commitMessage: "feat: add ${{ parameters.name }} application"
# Step 5: Create ArgoCD Application
- id: create-argocd-app
name: Create ArgoCD Application
action: kubernetes:apply
input:
namespaced: true
manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ${{ parameters.name }}
namespace: argocd
labels:
owner: ${{ parameters.owner }}
spec:
project: ${{ parameters.owner }}
source:
repoURL: https://github.com/acme/gitops-repo.git
path: apps/overlays/${{ parameters.environment }}/${{ parameters.name }}
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: ${{ parameters.environment }}
syncPolicy:
automated:
prune: true
selfHeal: true
# Step 6: Register in catalog
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['create-repo'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps['create-repo'].output.remoteUrl }}
- title: GitOps PR
url: ${{ steps['gitops-pr'].output.pullRequestUrl }}
- title: View in Catalog
icon: catalog
entityRef: ${{ steps['register'].output.entityRef }}
GitOps Repository Structure
gitops-repo/
├── apps/
│ ├── base/
│ │ └── user-service/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── rollout.yaml # Argo Rollout
│ │ └── kustomization.yaml
│ └── overlays/
│ ├── development/
│ │ └── user-service/
│ │ ├── database-claim.yaml # Crossplane
│ │ └── kustomization.yaml
│ ├── staging/
│ └── production/
│
├── infrastructure/
│ ├── crossplane/
│ │ ├── xrds/
│ │ │ └── database-xrd.yaml
│ │ ├── compositions/
│ │ │ ├── database-aws.yaml
│ │ │ └── database-gcp.yaml
│ │ └── provider-configs/
│ └── argocd/
│ ├── projects/
│ └── applicationsets/
│
└── clusters/
└── production/
├── apps.yaml
└── infrastructure.yaml
Unified Application Manifest
Combine app deployment with Rollout and infrastructure:
# apps/base/user-service/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- rollout.yaml
- service.yaml
- service-canary.yaml
- ingress.yaml
# apps/overlays/production/user-service/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../base/user-service
- database-claim.yaml # Crossplane claim
- analysis-template.yaml # Argo Rollouts analysis
patches:
- path: patch-replicas.yaml
- path: patch-image.yaml
# apps/overlays/production/user-service/database-claim.yaml
apiVersion: platform.acme.com/v1alpha1
kind: Database
metadata:
name: user-service-db
spec:
size: medium
engine: postgres
version: "15"
writeConnectionSecretToRef:
name: user-service-db-credentials
ApplicationSet for Everything
Manage all components with one ApplicationSet:
# infrastructure/argocd/applicationsets/all-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: all-platform-apps
namespace: argocd
spec:
generators:
- matrix:
generators:
- list:
elements:
- environment: development
cluster: https://dev.k8s.acme.com
- environment: staging
cluster: https://staging.k8s.acme.com
- environment: production
cluster: https://prod.k8s.acme.com
- git:
repoURL: https://github.com/acme/gitops-repo.git
revision: main
directories:
- path: apps/overlays/{{environment}}/*
template:
metadata:
name: '{{environment}}-{{path.basename}}'
namespace: argocd
labels:
environment: '{{environment}}'
app: '{{path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/acme/gitops-repo.git
targetRevision: main
path: '{{path}}'
destination:
server: '{{cluster}}'
namespace: '{{path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
End-to-End Workflow
Developer Workflow:
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. Developer opens Backstage │
│ └─► "Create Full-Stack Service" │
│ │
│ 2. Fills form: │
│ └─► name: payment-service │
│ └─► database: medium │
│ └─► environment: production │
│ │
│ 3. Backstage creates: │
│ └─► GitHub repo with code template │
│ └─► PR to gitops-repo with configs │
│ └─► Crossplane Database claim │
│ └─► ArgoCD Application │
│ └─► Catalog entry │
│ │
│ 4. PR merged → ArgoCD syncs: │
│ └─► Deployment/Rollout created │
│ └─► Crossplane provisions RDS │
│ └─► Connection secret available │
│ │
│ 5. Service running with: │
│ └─► Progressive delivery (canary) │
│ └─► Database ready │
│ └─► Full observability │
│ │
│ Time: ~10 minutes (fully automated) │
│ │
└─────────────────────────────────────────────────────────┘
In the next module, we'll explore platform observability and reliability patterns. :::