diff --git a/LAB06.md b/LAB06.md new file mode 100644 index 0000000..1b89d39 --- /dev/null +++ b/LAB06.md @@ -0,0 +1,822 @@ +# LAB06: Deploying with Crossplane + +Welcome to LAB06! In this lab, you'll learn how to provision cloud resources using Crossplane — the Kubernetes-native control plane framework. By the end of this lab, you'll have: + +- Installed Crossplane in your Kind cluster +- Configured the Azure provider using your existing Service Principal +- Deployed Azure resources directly as Managed Resources +- Created an abstraction layer (Composition) so developers use a simple API +- Delivered self-service infrastructure via Claims and ArgoCD GitOps +- (Stretch) Provisioned a GitHub repository using the GitHub provider + +## Prerequisites + +Before starting, ensure you have completed: +- ✅ **LAB01**: Your local environment should have: + - Kind cluster running with NGINX ingress + - ArgoCD installed and accessible + - `kubectl` configured and working +- ✅ **LAB02**: Multi-tenant ArgoCD setup with: + - Self-service ArgoCD projects configured + - Understanding of GitOps workflows +- ✅ **LAB03**: Azure integration with: + - Azure CLI installed and configured + - Azure Service Principal created (we'll reuse this) + - Azure subscription access + +**Additional Requirements for this lab:** +- ✅ **Helm**: Kubernetes package manager (used in LAB03 — already installed) +- ✅ **kubectl**: Already installed from LAB01 + +## Overview + +In LAB03 we used Azure Service Operator (ASO) — Microsoft's operator that translates Kubernetes manifests into ARM API calls. In LAB05 we used Terranetes to bring Terraform/OpenTofu into Kubernetes. Now we'll explore a third approach: **Crossplane**. + +### What is Crossplane? + +Crossplane is a CNCF project (graduated 2024) that turns your Kubernetes cluster into a **universal control plane** for cloud infrastructure. It provides: + +- **Managed Resources**: 1:1 mappings to cloud resources (like ASO, but provider-agnostic) +- **Compositions**: Platform engineering abstractions — combine multiple resources behind a single API +- **Claims**: The developer-facing API — simple, opinionated, self-service +- **Multi-Provider**: One cluster can manage Azure, AWS, GCP, GitHub, and 100+ providers simultaneously + +### Crossplane vs ASO vs Terranetes + +| Feature | ASO (LAB03) | Terranetes (LAB05) | Crossplane (LAB06) | +|---------|-------------|-------------------|-------------------| +| Approach | Kubernetes operator | IaC (Terraform) in K8s | Control plane framework | +| Abstraction | CRDs only | Revisions / Plans | XRD + Composition + Claims | +| Multi-provider | Azure only | Any Terraform provider | 100+ native providers | +| State management | Azure (ARM) | Terraform state in K8s | Kubernetes etcd | +| Best for | Azure-only shops | Existing Terraform users | Platform engineering teams | + +### What We'll Build + +``` +Git Claim → ArgoCD → AppStorageClaim → Composition → ResourceGroup + StorageAccount (Azure) +``` + +## Part 1: Install Crossplane + +### Install Crossplane via Helm + +Crossplane is distributed as a Helm chart from its stable repository. + +```bash +# Add the Crossplane Helm repository +helm repo add crossplane-stable https://charts.crossplane.io/stable +helm repo update + +# Install Crossplane in its own namespace +helm install crossplane \ + --namespace crossplane-system \ + --create-namespace \ + crossplane-stable/crossplane \ + --wait + +# Verify Crossplane is running +kubectl get pods -n crossplane-system +``` + +**Expected Output:** +``` +NAME READY STATUS RESTARTS AGE +crossplane-xxxxxxxxxx-xxxxx 1/1 Running 0 60s +crossplane-rbac-manager-xxxxxxxxxx-xxxxx 1/1 Running 0 60s +``` + +### Install the Crossplane CLI (kubectl plugin) + +The Crossplane CLI (`crossplane`) provides useful commands for managing packages and debugging. + +#### macOS +```bash +# Using Homebrew +brew install crossplane/tap/crossplane +``` + +#### Linux +```bash +# Download the latest release +curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | sh +sudo mv crossplane /usr/local/bin +``` + +#### Windows +```powershell +# Using Chocolatey +choco install crossplane-cli + +# Or download from GitHub releases +# https://github.com/crossplane/crossplane/releases +``` + +#### Verify Installation +```bash +crossplane --version +# Example: v1.15.0 +``` + +### Verification Steps - Part 1 + +```bash +# Check Crossplane pods are Running +kubectl get pods -n crossplane-system + +# Check Crossplane CRDs are installed +kubectl get crds | grep crossplane.io + +# Verify the CLI +crossplane --version +``` + +**Expected Output:** +- Two pods in Running state (crossplane and crossplane-rbac-manager) +- Several CRDs including `providers.pkg.crossplane.io`, `configurations.pkg.crossplane.io` +- Crossplane CLI version number + +### Reflection Questions - Part 1 + +1. **Package Manager**: Crossplane uses a built-in package manager for providers. How does this differ from installing operators with `kubectl apply`? + +2. **RBAC Manager**: Why does Crossplane run a separate RBAC manager alongside the main controller? + +3. **Control Plane Philosophy**: Crossplane describes itself as "the cloud native control plane framework." What does "control plane" mean in this context, and how does it differ from a typical Kubernetes operator? + +## Part 2: Configure the Azure Provider + +### Install the Azure Provider Family + +Crossplane uses **providers** — packages that add support for a specific platform (Azure, AWS, GitHub, etc.). The Upbound Azure provider family is the official, best-maintained option. + +```bash +# Install the Azure providers (family + storage sub-provider) +kubectl apply -f lab06/crossplane/provider-family-azure.yaml + +# Watch the providers install (this pulls provider packages — takes ~2 minutes) +kubectl get providers -w +``` + +You should see the providers move from `Installing` to `Healthy`: +``` +NAME INSTALLED HEALTHY PACKAGE AGE +upbound-provider-azure-storage True True xpkg.upbound.io/upbound/provider-azure-storage:v1 2m +upbound-provider-family-azure True True xpkg.upbound.io/upbound/provider-family-azure:v1 2m +``` + +> **Note**: The family provider installs CRDs for all Azure services. Using sub-providers (like `provider-azure-storage`) reduces the CRD footprint and speeds up installation. + +### Create Azure Credentials Secret + +We'll reuse the Service Principal from LAB03. The SP credentials need to be in a JSON format that Crossplane understands. + +```bash +# Retrieve your Service Principal details from LAB03 +# If you saved them as environment variables: +SP_CLIENT_ID="your-sp-client-id" +SP_CLIENT_SECRET="your-sp-client-secret" +SP_TENANT_ID=$(az account show --query tenantId -o tsv) +SP_SUBSCRIPTION_ID=$(az account show --query id -o tsv) + +# Create the credentials JSON file +cat > /tmp/azure-sp.json << EOF +{ + "clientId": "${SP_CLIENT_ID}", + "clientSecret": "${SP_CLIENT_SECRET}", + "subscriptionId": "${SP_SUBSCRIPTION_ID}", + "tenantId": "${SP_TENANT_ID}", + "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", + "resourceManagerEndpointUrl": "https://management.azure.com/", + "activeDirectoryGraphResourceId": "https://graph.windows.net/", + "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", + "galleryEndpointUrl": "https://gallery.azure.com/", + "managementEndpointUrl": "https://management.core.windows.net/" +} +EOF + +# Verify the JSON is valid +cat /tmp/azure-sp.json | python3 -m json.tool > /dev/null && echo "JSON is valid" + +# Create the Kubernetes secret in crossplane-system +kubectl create secret generic azure-sp-creds \ + --from-literal=credentials="$(cat /tmp/azure-sp.json)" \ + --namespace crossplane-system + +# Clean up the temporary file +rm /tmp/azure-sp.json + +# Verify the secret was created +kubectl get secret azure-sp-creds -n crossplane-system +``` + +> **Security Note**: Never commit the Service Principal credentials to Git. The secret lives only in your Kubernetes cluster. + +### Apply the ProviderConfig + +```bash +kubectl apply -f lab06/crossplane/providerconfig-azure.yaml + +# Verify the ProviderConfig is ready +kubectl get providerconfigs +``` + +**Expected Output:** +``` +NAME AGE SYNCED +default 10s True +``` + +### Verification Steps - Part 2 + +```bash +# All providers should be Healthy +kubectl get providers + +# ProviderConfig should be Synced +kubectl get providerconfigs + +# Check for any issues +kubectl describe providerconfig default +``` + +**Check for Azure CRDs from the provider:** +```bash +kubectl get crds | grep azure.upbound.io | head -10 +``` + +### Reflection Questions - Part 2 + +1. **ProviderConfig vs Provider**: What is the difference between a `Provider` resource and a `ProviderConfig` resource in Crossplane? + +2. **Credential Security**: We stored the SP credentials in a Kubernetes Secret. What are the security implications? How would you improve this in production (hint: look up Crossplane's External Secrets integration)? + +3. **Provider Versioning**: The manifest pins the provider to `v1`. In production, should you pin to a specific version like `v1.5.2`? Why or why not? + +## Part 3: Deploy Managed Resources Directly + +**Managed Resources (MRs)** are the lowest-level Crossplane abstraction. Each MR maps directly to one Azure resource. This is similar to how ASO CRDs work in LAB03, but provider-agnostic. + +### Update the Resource Names + +Before applying, edit the manifests to use unique names: + +```bash +# Edit the ResourceGroup manifest +# Replace "yourname" in crossplane.io/external-name +nano lab06/crossplane/managed-resources/resourcegroup.yaml +``` + +Use something like `rg-alice-crossplane-workshop` (replace `alice` with your name). + +For the StorageAccount: +```bash +nano lab06/crossplane/managed-resources/storageaccount.yaml +``` + +Use something like `stalicecpworkshop` (storage account names: 3-24 lowercase alphanumeric, globally unique). + +### Deploy the ResourceGroup + +```bash +kubectl apply -f lab06/crossplane/managed-resources/resourcegroup.yaml + +# Watch the reconciliation +kubectl get resourcegroup -w +``` + +You will see the status progress: +``` +NAME READY SYNCED EXTERNAL-NAME AGE +workshop-rg False True rg-yourname-crossplane-workshop 5s +workshop-rg True True rg-yourname-crossplane-workshop 45s +``` + +**When READY=True**, Crossplane has successfully created the Resource Group in Azure. + +```bash +# Verify in Azure +az group list --query "[?name=='rg-yourname-crossplane-workshop']" -o table +``` + +### Deploy the StorageAccount + +```bash +kubectl apply -f lab06/crossplane/managed-resources/storageaccount.yaml + +# Watch the reconciliation +kubectl get accounts -w +``` + +```bash +# Describe the resource to see detailed status and any errors +kubectl describe accounts workshop-sa + +# Verify in Azure +az storage account list --query "[?name=='styournamecpworkshop']" -o table +``` + +### Understand the Reconciliation Loop + +Crossplane continuously reconciles the desired state (your YAML) against the actual state (Azure). Test this: + +```bash +# 1. Find your Resource Group in Azure Portal and add a tag manually +# (or via az CLI): +az group update \ + --name rg-yourname-crossplane-workshop \ + --set tags.manual-tag=oops + +# 2. Wait ~30 seconds, then check: +az group show --name rg-yourname-crossplane-workshop --query tags + +# Crossplane will remove the manual tag because it's not in the YAML spec! +``` + +> **Key Insight**: This is the **control plane** model. The Kubernetes spec is the source of truth — any drift is automatically corrected. + +### Verification Steps - Part 3 + +```bash +# All Managed Resources should show READY=True, SYNCED=True +kubectl get resourcegroup,accounts + +# Inspect the MR to see Azure-side details +kubectl describe resourcegroup workshop-rg + +# Check the Azure side independently +az group show --name rg-yourname-crossplane-workshop -o table +az storage account show --name styournamecpworkshop -o table +``` + +### Reflection Questions - Part 3 + +1. **ASO vs Crossplane MRs**: Compare the ResourceGroup YAML in this lab with the ASO ResourceGroup YAML from LAB03. What are the key structural differences? + +2. **Drift Correction**: We demonstrated that Crossplane corrects drift automatically. When is this useful? When might it cause problems? + +3. **Annotations vs Spec**: The Azure resource name is set via `crossplane.io/external-name` annotation rather than in `spec`. Why do you think Crossplane uses this pattern? + +## Part 4: Create a Composition + +This is **the Crossplane superpower**. Instead of requiring developers to know about Resource Groups and Storage Accounts, we create a higher-level `AppStorage` concept — a platform abstraction owned by the platform team. + +### Understand the Pieces + +Crossplane compositions involve three resources: + +| Resource | Who creates it | Purpose | +|----------|---------------|---------| +| **XRD** (CompositeResourceDefinition) | Platform team | Defines the new API (AppStorage concept) | +| **Composition** | Platform team | Maps AppStorage → specific Azure resources | +| **XR / Claim** | Developer | Requests an AppStorage instance | + +### Apply the XRD + +```bash +kubectl apply -f lab06/crossplane/composition/xrd.yaml + +# Verify the new CRD was created +kubectl get xrds +kubectl get crds | grep platform.workshop.io +``` + +**Expected Output:** +``` +NAME ESTABLISHED OFFERED AGE +xappstorages.platform.workshop.io True True 10s +``` + +Two new CRDs are now available in your cluster: +- `xappstorages.platform.workshop.io` — the Composite Resource (cluster-scoped, for platform team) +- `appstorageclaims.platform.workshop.io` — the Claim (namespace-scoped, for application teams) + +### Apply the Composition + +```bash +kubectl apply -f lab06/crossplane/composition/composition.yaml + +# Verify the Composition is installed and references the correct XRD +kubectl get compositions +kubectl describe composition appstorage-composition +``` + +**Expected Output:** +``` +NAME XR-KIND XR-APIVERSION AGE +appstorage-composition XAppStorage platform.workshop.io/v1alpha1 5s +``` + +### Test the Composition with a Direct XR (Optional) + +Before exposing Claims to developers, you can test the Composition directly using a Composite Resource (XR). XRs are cluster-scoped — useful for platform team testing. + +```bash +# Create a test XR +cat < crossplane-claims/app2-storage-claim.yaml +apiVersion: platform.workshop.io/v1alpha1 +kind: AppStorageClaim +metadata: + name: app2-storage + namespace: team-alpha +spec: + parameters: + resourceGroupName: rg-yourname-crossplane-app2 + storageAccountName: styournamecpapp2 + location: swedencentral + environment: workshop + compositionRef: + name: appstorage-composition +EOF + +git add crossplane-claims/app2-storage-claim.yaml +git commit -m "feat: add AppStorageClaim for app2" +git push origin main + +# ArgoCD will automatically sync and Crossplane will provision the resources +argocd app sync crossplane-claims +kubectl get appstorageclaims -n team-alpha -w +``` + +### Verification Steps - Part 5 + +```bash +# Claims are bound and ready +kubectl get appstorageclaims -n team-alpha + +# ArgoCD application is Synced and Healthy +argocd app get crossplane-claims + +# All MRs are READY=True +kubectl get resourcegroups,accounts + +# Azure resources exist +az group list --query "[?tags.\"managed-by\"=='crossplane']" -o table +``` + +### Reflection Questions - Part 5 + +1. **Developer Experience**: A developer now creates infrastructure by committing a 15-line YAML file with 3 parameters. Compare this to the traditional process (raise a ticket → ops team → Terraform PR → review → apply). What does this mean for developer velocity? + +2. **Platform Boundaries**: The Claim API exposes `resourceGroupName`, `storageAccountName`, `location`, and `environment`. As a platform engineer, what parameters would you *not* expose to developers, and why (hint: think `accountTier`, `accountReplicationType`, security settings)? + +3. **GitOps + Crossplane**: ArgoCD manages the *desired state* of Claims in the cluster. Crossplane manages the *desired state* of Azure resources. How do these two control loops complement each other? + +## Part 6 (Stretch): Add the GitHub Provider + +This stretch goal demonstrates Crossplane's multi-provider capability: one cluster managing both Azure **and** GitHub resources, potentially from a single Claim. + +### Install the GitHub Provider + +```bash +# Apply the GitHub provider manifest (includes provider + ProviderConfig + example repo) +# Note: Apply just the Provider first — you need to create credentials before ProviderConfig + +# Apply only the Provider resource +kubectl apply -f - <&1 || echo "Namespace deleted" +``` + +### Final Verification + +```bash +# Confirm no Crossplane resources remain +kubectl get providers 2>&1 +kubectl get crds | grep crossplane.io +kubectl get crds | grep platform.workshop.io + +# Confirm Azure resources are cleaned up +az group list --query "[?tags.\"managed-by\"=='crossplane']" -o table +``` + +## Summary + +In this lab, you explored Crossplane's layered architecture: + +| Layer | Resource | Audience | +|-------|----------|----------| +| Infrastructure | Managed Resources (ResourceGroup, Account) | Crossplane / Azure | +| Abstraction | XRD + Composition | Platform engineers | +| Self-service | Claims (AppStorageClaim) | Application developers | +| GitOps | ArgoCD Application | Platform engineers | + +### Key Takeaways + +1. **Crossplane = control plane**: It continuously reconciles desired state, unlike one-shot IaC tools +2. **Compositions are platform APIs**: They hide complexity and enforce platform standards +3. **Claims = developer experience**: Developers never need to know about ResourceGroups or ProviderConfigs +4. **Multi-provider strength**: One cluster, one control plane, any cloud or SaaS provider +5. **GitOps integration**: Crossplane claims flow naturally through ArgoCD just like application manifests + +### Next Steps + +- Explore more Azure providers: `provider-azure-network`, `provider-azure-sql` +- Add policy enforcement with [Crossplane Composition Functions](https://docs.crossplane.io/latest/concepts/composition-functions/) +- Integrate with External Secrets Operator for credential management +- Look at [Upbound Spaces](https://docs.upbound.io/spaces/) for production-grade Crossplane hosting +- Compare with [Kratix](https://kratix.io/) — another platform engineering framework built on Crossplane + +## Additional Resources + +- [Crossplane Documentation](https://docs.crossplane.io/) +- [Upbound Marketplace (providers)](https://marketplace.upbound.io/) +- [Crossplane Slack](https://slack.crossplane.io/) +- [CNCF Crossplane Project](https://www.cncf.io/projects/crossplane/) +- [Azure Provider Reference](https://marketplace.upbound.io/providers/upbound/provider-family-azure) +- [GitHub Provider Reference](https://marketplace.upbound.io/providers/crossplane-contrib/provider-github) diff --git a/README.md b/README.md index f2926a9..1e41223 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,13 @@ Workshop for Platform Engineering in a day - Convert traditional IaC workflows to GitOps with Terranetes - Implement platform engineering patterns for self-service infrastructure +### [LAB06: Deploying with Crossplane](LAB06.md) +- Install Crossplane in your Kind cluster and configure the Azure provider +- Deploy Azure resources directly as Managed Resources +- Create platform abstractions using XRDs and Compositions +- Enable developer self-service via Claims synced through ArgoCD +- (Stretch) Provision GitHub repositories with the GitHub provider + #### Comments or feedback how to improve let us know before you go home or connect here: - https://www.linkedin.com/in/geertvandercruijsen/ - https://www.linkedin.com/in/cvs79/ diff --git a/lab06/README.md b/lab06/README.md new file mode 100644 index 0000000..24432e6 --- /dev/null +++ b/lab06/README.md @@ -0,0 +1,139 @@ +# LAB06: Deploying with Crossplane + +This directory contains all the configuration files needed for LAB06. + +## Directory Structure + +``` +lab06/ +└── crossplane/ + ├── provider-family-azure.yaml # Install Azure provider family + ├── providerconfig-azure.yaml # Azure ProviderConfig (credentials reference) + │ + ├── managed-resources/ # Direct Managed Resource approach (Part 3) + │ ├── resourcegroup.yaml # Azure ResourceGroup MR + │ └── storageaccount.yaml # Azure StorageAccount MR + │ + ├── composition/ # Abstraction layer (Part 4) + │ ├── xrd.yaml # CompositeResourceDefinition (XRD) + │ └── composition.yaml # Composition: XAppStorage → RG + SA + │ + ├── claims/ # Developer-facing API (Part 5) + │ └── app-storage-claim.yaml # Example AppStorageClaim + │ + ├── argocd/ # GitOps integration (Part 5) + │ └── argocd-application.yaml # ArgoCD Application for Claims + │ + └── stretch/ # Stretch goal (Part 6) + └── provider-github.yaml # GitHub provider + example Repository MR +``` + +## Quick Start + +### Install Crossplane + +```bash +# Add the Crossplane Helm repo +helm repo add crossplane-stable https://charts.crossplane.io/stable +helm repo update + +# Install Crossplane +helm install crossplane \ + --namespace crossplane-system \ + --create-namespace \ + crossplane-stable/crossplane + +# Verify pods +kubectl get pods -n crossplane-system +``` + +### Install Azure Provider + +```bash +kubectl apply -f lab06/crossplane/provider-family-azure.yaml + +# Wait until providers are healthy +kubectl get providers -w +``` + +### Configure Azure Credentials + +```bash +# Create the Service Principal credentials secret (reuse from LAB03) +kubectl create secret generic azure-sp-creds \ + --from-literal=credentials="$(cat azure-sp.json)" \ + --namespace crossplane-system + +# Apply the ProviderConfig +kubectl apply -f lab06/crossplane/providerconfig-azure.yaml +``` + +### Deploy Managed Resources Directly (Part 3) + +```bash +# Edit resourcegroup.yaml and storageaccount.yaml to set unique names first +kubectl apply -f lab06/crossplane/managed-resources/resourcegroup.yaml +kubectl get resourcegroup -w # Wait for READY=True + +kubectl apply -f lab06/crossplane/managed-resources/storageaccount.yaml +kubectl get accounts -w +``` + +### Deploy via Composition (Parts 4-5) + +```bash +# Install the XRD and Composition +kubectl apply -f lab06/crossplane/composition/xrd.yaml +kubectl apply -f lab06/crossplane/composition/composition.yaml + +# Edit app-storage-claim.yaml to set unique names, then: +kubectl apply -f lab06/crossplane/claims/app-storage-claim.yaml +kubectl get appstorageclaims -n team-alpha -w +``` + +### GitOps with ArgoCD (Part 5) + +```bash +# Edit argocd-application.yaml to point at your Git repository, then: +kubectl apply -f lab06/crossplane/argocd/argocd-application.yaml -n argocd + +# Check the application in ArgoCD +argocd app get crossplane-claims +``` + +## Prerequisites + +- Completed LAB01 (Kind cluster + ArgoCD running) +- Completed LAB02 (GitOps repo + ArgoCD ApplicationSets) +- Completed LAB03 (Azure Service Principal + Azure CLI configured) + +## What Gets Created + +| Resource | Type | Description | +|----------|------|-------------| +| Azure Resource Group | Managed Resource | Logical container for Azure resources | +| Azure Storage Account | Managed Resource | Blob/file storage with LRS replication | +| XAppStorage / AppStorageClaim | Custom CRD | Platform abstraction over RG + SA | +| GitHub Repository (stretch) | Managed Resource | Private GitHub repo via provider-github | + +## Cleanup + +```bash +# Delete claims first — Crossplane cascades to Managed Resources and Azure +kubectl delete appstorageclaims --all -n team-alpha + +# Delete direct Managed Resources (if created in Part 3) +kubectl delete accounts workshop-sa +kubectl delete resourcegroup workshop-rg + +# Uninstall providers +kubectl delete provider upbound-provider-azure-storage +kubectl delete provider upbound-provider-family-azure + +# Uninstall Crossplane +helm uninstall crossplane -n crossplane-system +``` + +## Documentation + +See [LAB06.md](../LAB06.md) for the full lab instructions with detailed explanations and verification steps. diff --git a/lab06/crossplane/argocd/argocd-application.yaml b/lab06/crossplane/argocd/argocd-application.yaml new file mode 100644 index 0000000..83288d9 --- /dev/null +++ b/lab06/crossplane/argocd/argocd-application.yaml @@ -0,0 +1,40 @@ +# ArgoCD Application - Self-Service Claims via GitOps +# This ArgoCD Application watches a Git directory for AppStorageClaim +# manifests. When a developer commits a new Claim file, ArgoCD syncs it +# to the cluster and Crossplane takes over to provision the Azure resources. +# +# This is the same GitOps pattern used in LAB02, extended to cloud infrastructure. +# +# Instructions: +# 1. Replace YOUR_GITHUB_ORG/YOUR_REPO with your repository details +# 2. Ensure the path exists in your repository (e.g., crossplane-claims/) +# 3. Apply with: kubectl apply -f argocd-application.yaml -n argocd + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: crossplane-claims + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: https://github.com/YOUR_GITHUB_ORG/YOUR_REPO.git + targetRevision: HEAD + # Directory in your repo where Claim YAMLs live + path: crossplane-claims + + destination: + server: https://kubernetes.default.svc + # Claims are namespace-scoped; ArgoCD creates the namespace if needed + namespace: team-alpha + + syncPolicy: + automated: + prune: true # Remove claims that are deleted from Git + selfHeal: true # Re-sync if someone edits the cluster manually + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/lab06/crossplane/claims/app-storage-claim.yaml b/lab06/crossplane/claims/app-storage-claim.yaml new file mode 100644 index 0000000..1a178e3 --- /dev/null +++ b/lab06/crossplane/claims/app-storage-claim.yaml @@ -0,0 +1,34 @@ +# AppStorageClaim - Example Claim for application teams +# A Claim is the developer-facing API. It is namespace-scoped and exposes +# only the parameters that application teams need to know about. +# The platform team's Composition handles all the complexity behind the scenes. +# +# Instructions: +# 1. Replace "yourname" placeholders with your own identifiers +# 2. Storage account names must be globally unique, lowercase, 3-24 chars +# 3. Apply with: kubectl apply -f app-storage-claim.yaml +# 4. Watch: kubectl get appstorageclaims -n team-alpha +# 5. List MRs: kubectl get resourcegroups,accounts + +apiVersion: v1 +kind: Namespace +metadata: + name: team-alpha + +--- +apiVersion: platform.workshop.io/v1alpha1 +kind: AppStorageClaim +metadata: + name: myapp-storage + namespace: team-alpha +spec: + parameters: + # Must be unique in your Azure subscription + resourceGroupName: rg-yourname-crossplane-claim + # Must be globally unique, lowercase, 3-24 chars + storageAccountName: styournamecpclaim + location: swedencentral + environment: workshop + # Bind this claim to the composition + compositionRef: + name: appstorage-composition diff --git a/lab06/crossplane/composition/composition.yaml b/lab06/crossplane/composition/composition.yaml new file mode 100644 index 0000000..3ae19b4 --- /dev/null +++ b/lab06/crossplane/composition/composition.yaml @@ -0,0 +1,89 @@ +# Composition for AppStorage +# The Composition tells Crossplane how to satisfy an AppStorage claim: +# it maps one XAppStorage → one ResourceGroup + one StorageAccount. +# This is the "Crossplane superpower" — platform engineering abstractions. +# +# Apply with: kubectl apply -f composition.yaml +# Check: kubectl get compositions + +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: appstorage-composition + labels: + provider: azure + type: storage +spec: + # Which XRD this Composition satisfies + compositeTypeRef: + apiVersion: platform.workshop.io/v1alpha1 + kind: XAppStorage + + # The resources that will be created for every XAppStorage/Claim + resources: + + # --- Resource 1: Azure Resource Group --- + - name: resource-group + base: + apiVersion: azure.upbound.io/v1beta1 + kind: ResourceGroup + spec: + forProvider: + location: swedencentral # Patched below + tags: + managed-by: crossplane + providerConfigRef: + name: default + patches: + # Patch the external (Azure) name from the claim parameter + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.resourceGroupName + toFieldPath: metadata.annotations["crossplane.io/external-name"] + # Patch location from the claim parameter + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.location + toFieldPath: spec.forProvider.location + # Patch environment tag + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.environment + toFieldPath: spec.forProvider.tags.environment + # Write the Azure Resource ID back to the XR status + - type: ToCompositeFieldPath + fromFieldPath: status.atProvider.id + toFieldPath: status.resourceGroupId + + # --- Resource 2: Azure Storage Account --- + - name: storage-account + base: + apiVersion: storage.azure.upbound.io/v1beta2 + kind: Account + spec: + forProvider: + location: swedencentral # Patched below + resourceGroupNameSelector: + matchControllerRef: true # Auto-select the RG created above + accountTier: Standard + accountReplicationType: LRS + enableHttpsTrafficOnly: true + minTlsVersion: TLS1_2 + tags: + managed-by: crossplane + providerConfigRef: + name: default + patches: + # Patch the globally unique storage account name + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.storageAccountName + toFieldPath: metadata.annotations["crossplane.io/external-name"] + # Patch location + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.location + toFieldPath: spec.forProvider.location + # Patch environment tag + - type: FromCompositeFieldPath + fromFieldPath: spec.parameters.environment + toFieldPath: spec.forProvider.tags.environment + # Write the Azure Resource ID back to the XR status + - type: ToCompositeFieldPath + fromFieldPath: status.atProvider.id + toFieldPath: status.storageAccountId diff --git a/lab06/crossplane/composition/xrd.yaml b/lab06/crossplane/composition/xrd.yaml new file mode 100644 index 0000000..08d5e0c --- /dev/null +++ b/lab06/crossplane/composition/xrd.yaml @@ -0,0 +1,86 @@ +# CompositeResourceDefinition (XRD) for AppStorage +# An XRD defines a new custom API — the "AppStorage" concept. +# Platform engineers own this definition; developers use the simpler +# Claim API that it exposes (XAppStorage → AppStorageClaim). +# +# Apply with: kubectl apply -f xrd.yaml +# Check: kubectl get xrds + +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xappstorages.platform.workshop.io +spec: + group: platform.workshop.io + + # The Composite Resource (XR) — used by platform engineers directly + names: + kind: XAppStorage + plural: xappstorages + + # The Claim — the simplified API exposed to application teams + claimNames: + kind: AppStorageClaim + plural: appstorageclaims + + # Which namespaces can use Claims (omit to allow all) + # defaultCompositionRef: + # name: appstorage-composition + + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: AppStorage specification + properties: + parameters: + type: object + description: Configuration parameters for the storage resources + required: + - resourceGroupName + - storageAccountName + - location + properties: + resourceGroupName: + type: string + description: >- + Name of the Azure Resource Group to create. + Must be unique within your Azure subscription. + storageAccountName: + type: string + description: >- + Globally unique name for the Storage Account (3-24 lowercase + alphanumeric characters). + minLength: 3 + maxLength: 24 + pattern: "^[a-z0-9]{3,24}$" + location: + type: string + description: Azure region for the resources (e.g. swedencentral). + default: swedencentral + environment: + type: string + description: Environment tag applied to all resources. + default: workshop + enum: + - dev + - staging + - prod + - workshop + required: + - parameters + status: + type: object + properties: + resourceGroupId: + type: string + description: Azure Resource ID of the provisioned Resource Group + storageAccountId: + type: string + description: Azure Resource ID of the provisioned Storage Account diff --git a/lab06/crossplane/managed-resources/resourcegroup.yaml b/lab06/crossplane/managed-resources/resourcegroup.yaml new file mode 100644 index 0000000..739071a --- /dev/null +++ b/lab06/crossplane/managed-resources/resourcegroup.yaml @@ -0,0 +1,24 @@ +# Azure ResourceGroup - Managed Resource +# A Managed Resource (MR) is the lowest-level Crossplane abstraction. +# It maps 1:1 to a real Azure resource. Crossplane reconciles this +# against Azure continuously. +# +# Apply with: kubectl apply -f resourcegroup.yaml +# Watch: kubectl get resourcegroup workshop-rg -w +# Describe: kubectl describe resourcegroup workshop-rg + +apiVersion: azure.upbound.io/v1beta1 +kind: ResourceGroup +metadata: + name: workshop-rg + annotations: + crossplane.io/external-name: rg-yourname-crossplane-workshop # Replace yourname +spec: + forProvider: + location: swedencentral + tags: + environment: workshop + managed-by: crossplane + lab: lab06 + providerConfigRef: + name: default diff --git a/lab06/crossplane/managed-resources/storageaccount.yaml b/lab06/crossplane/managed-resources/storageaccount.yaml new file mode 100644 index 0000000..0a4b58e --- /dev/null +++ b/lab06/crossplane/managed-resources/storageaccount.yaml @@ -0,0 +1,35 @@ +# Azure StorageAccount - Managed Resource +# This StorageAccount is a direct Managed Resource that references the +# ResourceGroup created alongside it. Crossplane will create it in Azure +# and continuously reconcile its desired state. +# +# Important: Apply the ResourceGroup first and wait for it to be READY +# before applying this manifest. +# +# Apply with: kubectl apply -f storageaccount.yaml +# Watch: kubectl get storageaccounts workshop-sa -w +# Describe: kubectl describe storageaccounts workshop-sa + +apiVersion: storage.azure.upbound.io/v1beta2 +kind: Account +metadata: + name: workshop-sa + annotations: + # Azure storage account names must be globally unique, lowercase, 3-24 chars + crossplane.io/external-name: stwsyournamecrossplane # Replace yourname (max 24 chars) +spec: + forProvider: + location: swedencentral + resourceGroupNameRef: + # Reference by Kubernetes name — Crossplane resolves to the Azure RG name + name: workshop-rg + accountTier: Standard + accountReplicationType: LRS + enableHttpsTrafficOnly: true + minTlsVersion: TLS1_2 + tags: + environment: workshop + managed-by: crossplane + lab: lab06 + providerConfigRef: + name: default diff --git a/lab06/crossplane/provider-family-azure.yaml b/lab06/crossplane/provider-family-azure.yaml new file mode 100644 index 0000000..2161072 --- /dev/null +++ b/lab06/crossplane/provider-family-azure.yaml @@ -0,0 +1,28 @@ +# Azure Provider for Crossplane +# This installs the Upbound provider-family-azure which gives Crossplane +# the ability to manage Azure resources via CRDs. +# +# Apply with: kubectl apply -f provider-family-azure.yaml +# Check status: kubectl get providers + +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: upbound-provider-family-azure +spec: + package: xpkg.upbound.io/upbound/provider-family-azure:v1 + packagePullPolicy: IfNotPresent + revisionActivationPolicy: Automatic + +--- +# Individual provider for Azure Storage resources +# This is a sub-provider of the family that installs only the Storage CRDs, +# keeping the footprint small for the workshop. +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: upbound-provider-azure-storage +spec: + package: xpkg.upbound.io/upbound/provider-azure-storage:v1 + packagePullPolicy: IfNotPresent + revisionActivationPolicy: Automatic diff --git a/lab06/crossplane/providerconfig-azure.yaml b/lab06/crossplane/providerconfig-azure.yaml new file mode 100644 index 0000000..3c780b8 --- /dev/null +++ b/lab06/crossplane/providerconfig-azure.yaml @@ -0,0 +1,26 @@ +# Azure ProviderConfig for Crossplane +# This tells Crossplane how to authenticate with Azure. +# We store the Service Principal credentials in a Kubernetes Secret and +# reference it here — the same SP created in LAB03. +# +# Prerequisites: +# Create the secret first (see LAB06.md Part 2): +# kubectl create secret generic azure-sp-creds \ +# --from-literal=credentials="$(cat azure-sp.json)" \ +# --namespace crossplane-system +# +# Apply with: kubectl apply -f providerconfig-azure.yaml +# Check status: kubectl get providerconfigs + +apiVersion: azure.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: default +spec: + # azure-credentials secret holds the Service Principal JSON + credentials: + source: Secret + secretRef: + namespace: crossplane-system + name: azure-sp-creds + key: credentials diff --git a/lab06/crossplane/stretch/provider-github.yaml b/lab06/crossplane/stretch/provider-github.yaml new file mode 100644 index 0000000..8d2ffbf --- /dev/null +++ b/lab06/crossplane/stretch/provider-github.yaml @@ -0,0 +1,63 @@ +# Crossplane GitHub Provider - Stretch Goal +# This installs the community GitHub provider for Crossplane, enabling +# provisioning of GitHub repositories as Managed Resources. +# Combined with the Azure provider, a single Claim can trigger both +# Azure *and* GitHub resources — the multi-provider superpower. +# +# Apply with: kubectl apply -f provider-github.yaml +# Check status: kubectl get providers + +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-github +spec: + package: xpkg.upbound.io/crossplane-contrib/provider-github:v0.1.0 + packagePullPolicy: IfNotPresent + revisionActivationPolicy: Automatic + +--- +# GitHub ProviderConfig +# Store your GitHub Personal Access Token in a Secret first: +# +# kubectl create secret generic github-pat \ +# --from-literal=credentials='{"token":"ghp_YOUR_PAT_HERE"}' \ +# --namespace crossplane-system +# +# Reuse the PAT created in LAB05 (same scopes: repo, delete_repo). + +apiVersion: github.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: default-github +spec: + credentials: + source: Secret + secretRef: + namespace: crossplane-system + name: github-pat + key: credentials + +--- +# Example: GitHub Repository Managed Resource +# Once the provider is healthy, provision a repo with a single manifest. +# +# Apply with: kubectl apply -f provider-github.yaml +# Watch: kubectl get repositories -w + +apiVersion: repo.github.upbound.io/v1alpha1 +kind: Repository +metadata: + name: workshop-crossplane-demo + annotations: + crossplane.io/external-name: workshop-crossplane-demo # GitHub repo name +spec: + forProvider: + description: "Created by Crossplane during Platform Engineering Workshop LAB06" + visibility: private + autoInit: true + hasIssues: true + hasWiki: false + deleteBranchOnMerge: true + providerConfigRef: + name: default-github