Crossplane can import existing cloud resources into its control plane, allowing you to manage them even if they weren’t initially created by Crossplane. This is particularly useful for bringing your pre-existing infrastructure under Crossplane’s declarative management.

Let’s see this in action with an example. Imagine you have an existing AWS S3 bucket that you want Crossplane to "observe."

First, ensure you have the AWS provider configured and installed in your Crossplane instance.

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws
spec:
  package: xpkg.upbound.io/upbound/provider-aws:v0.43.0 # Example version

Then, apply the provider configuration.

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds

Now, to import an existing S3 bucket, you don’t create a Bucket resource directly. Instead, you create an Infrastructure resource that points to the existing bucket and then create a Composition that uses this Infrastructure.

Here’s how you’d define the Infrastructure for an existing S3 bucket. Notice the externalName field, which is crucial for referencing the actual cloud resource.

apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: my-imported-bucket # This is the name Crossplane will use internally
spec:
  forProvider:
    region: us-east-1 # Must match the region of your existing bucket
  providerConfigRef:
    name: default
  # This is the key for importing: specify the name of the existing resource
  # Crossplane will look for a bucket named 'my-preexisting-bucket-name' in AWS
  externalName: my-preexisting-bucket-name

After applying this Bucket manifest, Crossplane will attempt to "claim" the existing S3 bucket named my-preexisting-bucket-name in your AWS account. If successful, the Bucket resource in Kubernetes will be updated with the current status and details of the actual S3 bucket.

To make this managed resource available through a higher-level abstraction, you’d typically use a CompositeResourceDefinition (XRD) and a Composition.

Let’s define a simple XRD for a "StorageBucket."

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: storagedbuckets.storage.example.com
spec:
  group: storage.example.com
  names:
    kind: StorageBucket
    plural: storagedbuckets
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                storageClassName:
                  type: string
                # This is how you pass parameters to the underlying managed resource
                bucketName:
                  type: string
              required:
                - storageClassName
                - bucketName
            status:
              type: object
              properties:
                bucketId:
                  type: string
                bucketArn:
                  type: string

Now, create a Composition that maps StorageBucket to the AWS Bucket managed resource.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: aws-storagebuckets
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: storage.example.com/v1alpha1
    kind: StorageBucket
  resources:
    - name: bucket
      base:
        apiVersion: s3.aws.upbound.io/v1beta1
        kind: Bucket
        spec:
          # Default region, can be overridden by composite resource spec
          forProvider:
            region: us-east-1
          providerConfigRef:
            name: default
      patches:
        # Map the composite resource's bucketName to the managed resource's externalName
        - fromFieldPath: spec.bucketName
          toFieldPath: spec.forProvider.bucketName # This field maps to externalName for S3 Bucket
          # For S3 Bucket, spec.forProvider.bucketName is directly used as externalName
          # If you were using a different resource, you might need a different patch
          # For example, for RDS instances, you'd patch spec.forProvider.identifier
        - fromFieldPath: spec.bucketName
          toFieldPath: metadata.name # Also map to internal Crossplane name
        # Map status fields from managed resource to composite resource
        - fromFieldPath: status.id
          toFieldPath: status.bucketId
          type: FromCompositeFieldPath
        - fromFieldPath: status.arn
          toFieldPath: status.bucketArn
          type: FromCompositeFieldPath

Finally, you can create an instance of your StorageBucket composite resource, specifying the bucketName that corresponds to your existing AWS S3 bucket.

apiVersion: storage.example.com/v1alpha1
kind: StorageBucket
metadata:
  name: my-managed-storage
spec:
  # This must match the externalName you set in the managed resource definition
  bucketName: my-preexisting-bucket-name
  storageClassName: standard

When you apply this StorageBucket manifest, Crossplane will find the aws-storagebuckets composition. This composition will then ensure that the AWS Bucket managed resource is configured to observe my-preexisting-bucket-name. The externalName field in the managed resource is the critical piece that tells Crossplane to look for and claim an existing resource by that specific name, rather than attempting to create a new one. The bucketName in the composite resource spec is then mapped to the spec.forProvider.bucketName field of the Bucket managed resource, which Crossplane interprets as the externalName for S3 buckets.

The most surprising thing about importing is that you don’t explicitly tell Crossplane "import this." Instead, you define the desired state of the managed resource, including its externalName, and Crossplane’s reconciliation loop detects that a resource with that external name already exists and proceeds to "adopt" or "observe" it. The act of defining the managed resource with the externalName pointing to an existing cloud entity is what initiates the import. Crossplane’s reconciliation logic, specifically its ability to detect drift and reconcile existing resources, handles the rest. It checks if a resource matching the externalName exists; if it does, Crossplane updates its internal state to reflect the existing resource’s configuration rather than attempting to create a new one.

The exact levers you control are primarily through the externalName field on the managed resource definition and how you map parameters from your composite resource to the spec.forProvider section of the managed resource. This mapping dictates which properties of the existing cloud resource Crossplane will track and, if configured, attempt to reconcile drift against. You are essentially telling Crossplane, "This is what I want this resource to look like, and here’s the specific name of the existing resource that should match this desired state."

The next concept you’ll likely encounter is managing the lifecycle of these imported resources, particularly how Crossplane handles updates and deletions when the underlying cloud resource was not originally provisioned by Crossplane.

Want structured learning?

Take the full Crossplane course →