Migrate Live Clusters to Crossplane v2
This guide demonstrates how to migrate active live clusters to v2 namespaced composites from v1-style cluster scoped composites without recreating cloud resources.
This guide doesn't cover how to migrate compositions themselves to v2. To migrate to v2 XRDs, see Migrate configurations to v2.
Prerequisites
- Though not strongly required, this guide assumes you are using Crossplane configuration packages to manage your XRDs and compositions
- v1 and v2 version of configuration package
- Both versions need to have
function-external-name-backup-restoredeployed at the end of their composition pipeline - Upgraded Crossplane v2 control plane with v1 version of the configuration package deployed
- XR instances of v1 configuration running in the v2 cluster
(
apiVersion: apiextensions.crossplane.io/v1for the XRD)
Step by step guide
For the sake of demonstration this guide runs through all the steps, including the initial setup. If you were to do this in an actual cluster, you can skip setting up the control plane.
Step 1: Set up control plane
Create a control plane with Crossplane v2:
up ctp create v2-migration-test --crossplane-version=2.1.1-up.1 --crossplane-channel=None
Add function-external-name-backup-restore to dependencies:
- apiVersion: pkg.crossplane.io/v1
kind: Function
package: xpkg.upbound.io/solutions/function-external-name-backup-restore
version: v0.1.0
Prepare your compositions by adding this to your composition pipeline:
- step: external-name-backup
functionRef:
name: solutions-function-external-name-backup-restore
credentials:
- name: aws-creds
source: Secret
secretRef:
namespace: crossplane-system
name: aws-dynamodb-creds
key: credentials
A current limitation exists where function-credentials can't be optional. Because the function supports other external stores like AWS DynamoDB, you need to create an empty placeholder secret. This guide uses a ConfigMap on the same cluster to store the external names. This step is required for both the v1 and v2 versions of your composition.
IMPORTANT: Don't add any new resource and keep all
crossplane.io/composition-resource-name the same. This is essential for a
successful restore. If you do this in an actual production setup, create a
backup with Upbound tooling before you proceed.
Step 2: Deploy v1 configuration and composite
This example uses configuration-aws-network. Configuration packages with the
required changes are already prepared.
Switch context:
up ctx ./v2-migration-test
Deploy the configuration:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: upbound-configuration-aws-network
spec:
package: xpkg.upbound.io/solutions/configuration-aws-network:v1.0.0-migration-test
EOF
Configure the provider using static credentials:
kubectl apply -f - <<EOF
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-creds
key: credentials
EOF
Deploy the credentials. This assumes you have the credentials in
~/.aws/credentials with the following format:
~/.aws/credentials
[default]
aws_access_key_id=A...
aws_secret_access_key=a...
Apply them:
kubectl create secret generic aws-creds -n crossplane-system \
--from-file=credentials=$HOME/.aws/credentials \
--dry-run=client -o yaml | kubectl apply -f -
Create placeholder credentials for the function:
kubectl create secret generic aws-dynamodb-creds \
--namespace=crossplane-system \
--from-literal=credentials=""
Assign permissions for the function to write to ConfigMaps:
SA=$(kubectl get sa -ncrossplane-system | grep function-external-name-backup-restore | awk '{print $1}')
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: function-external-name-backup-restore-configmap
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "create", "update", "delete"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: function-external-name-backup-restore-configmap
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: function-external-name-backup-restore-configmap
subjects:
- kind: ServiceAccount
name: ${SA}
namespace: crossplane-system
EOF
Deploy the composite:
kubectl apply -f - <<EOF
apiVersion: aws.platform.upbound.io/v1alpha1
kind: XNetwork
metadata:
name: configuration-aws-network
namespace: network-team
annotations:
fn.crossplane.io/enable-external-store: "true"
fn.crossplane.io/cluster-id: "migration-poc"
fn.crossplane.io/store-type: "k8sconfigmap"
spec:
parameters:
id: configuration-aws-network
deletionPolicy: Orphan
region: us-west-2
vpcCidrBlock: 192.168.0.0/16
subnets:
- availabilityZone: us-west-2a
type: public
cidrBlock: 192.168.0.0/18
- availabilityZone: us-west-2b
type: public
cidrBlock: 192.168.64.0/18
- availabilityZone: us-west-2a
type: private
cidrBlock: 192.168.128.0/18
- availabilityZone: us-west-2b
type: private
cidrBlock: 192.168.192.0/18
EOF
Migration to namespaced managed resources requires dropping managed resources,
so they need to be orphaned. The recommended way is to use
deletionPolicy: Orphan (only supported in v1) or omit Delete in
managementPolicies (managementPolicies: ["Observe", "Create", "Update", "LateInitialize"]).
The function only backs up external names for resources that are set to be
orphaned by either method.
Step 3: Verify everything is prepared
Run the trace command to check all resources are synced and ready:
crossplane beta trace xnetwork configuration-aws-network
Expected output:
NAME SYNCED READY STATUS
XNetwork/configuration-aws-network True True Available
├─ InternetGateway/configuration-aws-network-971272c3bfda True True Available
├─ MainRouteTableAssociation/configuration-aws-network-18c857fe1af8 True True Available
├─ RouteTableAssociation/configuration-aws-network-03d02afa8678 True True Available
├─ RouteTableAssociation/configuration-aws-network-617ef61230c5 True True Available
├─ RouteTableAssociation/configuration-aws-network-671f50b8bb04 True True Available
├─ RouteTableAssociation/configuration-aws-network-fc8889923717 True True Available
├─ RouteTable/configuration-aws-network-0df02cd98a5a True True Available
├─ SecurityGroupRule/configuration-aws-network-99aa788bad71 True True Available
├─ SecurityGroupRule/configuration-aws-network-df5c35f2d876 True True Available
├─ SecurityGroup/configuration-aws-network-1ce1205df468 True True Available
├─ Subnet/configuration-aws-network-05dbaad53374 True True Available
├─ Subnet/configuration-aws-network-3787dda7db1f True True Available
├─ Subnet/configuration-aws-network-80321cfd754b True True Available
├─ Subnet/configuration-aws-network-c278f9a1db3d True True Available
├─ VPC/configuration-aws-network-0b68c47236cd True True Available
└─ Route/configuration-aws-network-006e6a6fe825 True True Available
Verify the ConfigMap contains all external names:
kubectl get configmap -ncrossplane-system external-name-backup-migration-poc -oyaml
The ConfigMap should show all external names written to it. The key is base64-encoded to work around limitations:
apiVersion: v1
data:
bm9uZS9ub25lL2F3cy5wbGF0Zm9ybS51cGJvdW5kLmlvL3YxYWxwaGExL1hOZXR3b3JrL2NvbmZpZ3VyYXRpb24tYXdzLW5ldHdvcms: '{"igw":{"externalName":"igw-037378105667837d2","resourceName":"configuration-aws-network-971272c3bfda"},...}'
kind: ConfigMap
metadata:
creationTimestamp: "2025-12-10T11:17:14Z"
labels:
app.kubernetes.io/managed-by: function-external-name-backup-restore
cluster-id: migration-poc
name: external-name-backup-migration-poc
namespace: crossplane-system
Step 4: Drop the XR
Delete the v1 XR:
kubectl delete xnetwork configuration-aws-network
Step 5: Update the configuration package
It's important that the function was added to the composition pipeline.
For this example, there is already a version prepared. Install the new version:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: upbound-configuration-aws-network
spec:
package: xpkg.upbound.io/solutions/configuration-aws-network:v2.0.0-migration-test
EOF
Verify the configuration has been updated successfully:
kubectl get configuration
Expected output:
NAME INSTALLED HEALTHY PACKAGE AGE
upbound-configuration-aws-network True True xpkg.upbound.io/solutions/configuration-aws-network:v2.0.0-migration-test 18m
Step 6: Deploy the v2 version of the XR
Configure the provider for namespaced resources:
kubectl create ns network-team
kubectl apply -f - <<EOF
apiVersion: aws.m.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
namespace: network-team
spec:
credentials:
source: Secret
secretRef:
namespace: network-team
name: aws-creds
key: credentials
EOF
kubectl create secret generic aws-creds -n network-team \
--from-file=credentials=$HOME/.aws/credentials \
--dry-run=client -o yaml | kubectl apply -f -
Deploy the v2 version of the XR:
kubectl apply -f - <<EOF
apiVersion: aws.platform.upbound.io/v1alpha1
kind: Network
metadata:
name: configuration-aws-network
namespace: network-team
annotations:
fn.crossplane.io/enable-external-store: "true"
fn.crossplane.io/cluster-id: "migration-poc"
fn.crossplane.io/store-type: "k8sconfigmap"
fn.crossplane.io/override-kind: "XNetwork"
fn.crossplane.io/override-namespace: "none"
fn.crossplane.io/require-restore: "true"
spec:
parameters:
managementPolicies:
- Create
- Update
- Observe
- LateInitialize
id: configuration-aws-network
region: us-west-2
vpcCidrBlock: 192.168.0.0/16
subnets:
- availabilityZone: us-west-2a
type: public
cidrBlock: 192.168.0.0/18
- availabilityZone: us-west-2b
type: public
cidrBlock: 192.168.64.0/18
- availabilityZone: us-west-2a
type: private
cidrBlock: 192.168.128.0/18
- availabilityZone: us-west-2b
type: private
cidrBlock: 192.168.192.0/18
EOF
Notice three critical settings:
fn.crossplane.io/override-kind: "XNetwork"- Because kind changed, tell the function to look up this kindfn.crossplane.io/override-namespace: "none"- Because namespace changed, the function needs to look up on cluster scopefn.crossplane.io/require-restore: "true"- Safety switch to not proceed unless all resources have been found in the backup store
Verify that everything was imported correctly:
crossplane beta trace network -nnetwork-team configuration-aws-network
Expected output:
NAME SYNCED READY STATUS
Network/configuration-aws-network (network-team) True True Available
├─ InternetGateway/configuration-aws-network-971272c3bfda (network-team) True True Available
├─ MainRouteTableAssociation/configuration-aws-network-18c857fe1af8 (network-team) True True Available
├─ RouteTableAssociation/configuration-aws-network-03d02afa8678 (network-team) True True Available
├─ RouteTableAssociation/configuration-aws-network-617ef61230c5 (network-team) True True Available
├─ RouteTableAssociation/configuration-aws-network-671f50b8bb04 (network-team) True True Available
├─ RouteTableAssociation/configuration-aws-network-fc8889923717 (network-team) True True Available
├─ RouteTable/configuration-aws-network-0df02cd98a5a (network-team) True True Available
├─ Route/configuration-aws-network-006e6a6fe825 (network-team) True True Available
├─ SecurityGroupRule/configuration-aws-network-99aa788bad71 (network-team) True True Available
├─ SecurityGroupRule/configuration-aws-network-df5c35f2d876 (network-team) True True Available
├─ SecurityGroup/configuration-aws-network-1ce1205df468 (network-team) True True Available
├─ Subnet/configuration-aws-network-05dbaad53374 (network-team) True True Available
├─ Subnet/configuration-aws-network-3787dda7db1f (network-team) True True Available
├─ Subnet/configuration-aws-network-80321cfd754b (network-team) True True Available
├─ Subnet/configuration-aws-network-c278f9a1db3d (network-team) True True Available
└─ VPC/configuration-aws-network-0b68c47236cd (network-team) True True Available
Step 7: Clean up
Drop the safety annotation from the XR:
kubectl annotate network configuration-aws-network \
-nnetwork-team fn.crossplane.io/require-restore-
Alternatively, delete all the annotations relating to
external-name-backup-restore to disable the function completely.
Delete the ConfigMap with the stored names:
kubectl delete configmap -ncrossplane-system external-name-backup-migration-poc
Finished
Congratulations, you migrated your XR to Crossplane v2 with a namespaced XR and namespaced managed resources without deleting or recreating any resources on AWS.
Best practices
Backup before migration
Always create a backup of your control plane before starting the migration. Use Upbound's backup tools or your preferred backup solution.
Test in non-production first
Test the migration process in a development or staging environment before running it in production.
Verify resource state
After migration, verify that all resources are in the expected state and that applications consuming the resources are working correctly.
Monitor during migration
Watch for any errors or warnings during the migration process. The function logs can provide useful debugging information.
Troubleshooting
Resources not being restored
Problem: Resources aren't being imported after v2 deployment.
Solution: Check that:
- The ConfigMap contains the external names
- The
fn.crossplane.io/require-restore: "true"annotation is set - The
override-kindandoverride-namespaceannotations are correct - All
crossplane.io/composition-resource-namevalues match between v1 and v2
Permission errors
Problem: Function can't write to ConfigMaps.
Solution: Verify the ClusterRole and ClusterRoleBinding are correctly set up for the function's ServiceAccount.
Missing external names in ConfigMap
Problem: ConfigMap doesn't contain all expected external names.
Solution: Ensure resources were set to orphan mode before deletion
(deletionPolicy: Orphan in v1 or appropriate managementPolicies in v2).
Next steps
- Migrate configurations to v2 - Technical guide for updating XRDs and compositions
- Upgrade to UXP - Upgrade from Crossplane v2 OSS to Upbound Crossplane
- Provider migration guide - Migrate from monolithic to family providers